def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked user logs the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='*****@*****.**') extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='*****@*****.**') user_w_map.save() user_wo_map.save() extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = [ '*****@*****.**', '*****@*****.**', 'testuser2@someother_idp.com' ] for idp in idps: for remote_user in remote_users: request = self.request_factory.get('/shib-login') request.session = import_module( settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({ 'Shib-Identity-Provider': idp, 'REMOTE_USER': remote_user, 'mail': remote_user }) request.user = AnonymousUser() response = shib_login(request) if idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsNotNone( ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/') elif idp == "https://someother.idp.com/" and remote_user in \ ['*****@*****.**', '*****@*****.**']: self.assertEqual(response.status_code, 403) self.assertIn( "You have already created an account using an external login", response.content) else: self.assertEqual(response.status_code, 200) self.assertContains(response, "<title>Register for")
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", internal_password="******", user=student, ) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org="Stanford", number="123", display_name="Shib Only", enrollment_domain="shib:https://idp.stanford.edu/", user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = { "path": "/shib-login/", "data": { "enrollment_action": "enroll", "course_id": course.id.to_deprecated_string(), "next": "/testredirect", }, "follow": False, "REMOTE_USER": "******", "Shib-Identity-Provider": "https://idp.stanford.edu/", } response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response["location"], "http://testserver/testredirect") # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response["location"], "http://testserver/testredirect") # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
def setUp(self): super(ExternalAuthShibTest, self).setUp() self.course = CourseFactory.create( org='Stanford', number='456', display_name='NO SHIB', user_id=self.user.id, ) self.shib_course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.user.id, ) self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save()
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') course.enrollment_domain = 'shib:https://idp.stanford.edu/' self.store.update_item(course, '**replace_user**') # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = { 'path': '/shib-login/', 'data': { 'enrollment_action': 'enroll', 'course_id': course.id, 'next': '/testredirect' }, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/' } response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = {'path': '/shib-login/', 'data': {'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect'}, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/'} response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET params """ if not settings.MITX_FEATURES.get('AUTH_USE_SHIB'): return student = UserFactory.create() extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') course.enrollment_domain = 'shib:https://idp.stanford.edu/' metadata = own_metadata(course) metadata['enrollment_domain'] = course.enrollment_domain self.store.update_metadata(course.location.url(), metadata) #use django test client for sessions and url processing #no enrollment before trying self.assertEqual( CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 0) self.client.logout() request_kwargs = { 'path': '/shib-login/', 'data': { 'enrollment_action': 'enroll', 'course_id': course.id }, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/' } response = self.client.get(**request_kwargs) #successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/') #now there is enrollment self.assertEqual( CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 1)
def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked user logs the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='*****@*****.**') extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='*****@*****.**') user_w_map.save() user_wo_map.save() extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = ['*****@*****.**', '*****@*****.**', 'testuser2@someother_idp.com'] for idp in idps: for remote_user in remote_users: request = self.request_factory.get('/shib-login') request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, 'REMOTE_USER': remote_user, 'mail': remote_user}) request.user = AnonymousUser() response = shib_login(request) if idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/') elif idp == "https://someother.idp.com/" and remote_user in \ ['*****@*****.**', '*****@*****.**']: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) else: self.assertEqual(response.status_code, 200) self.assertContains(response, "<title>Register for")
def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have the proper external auth """ # create 2 course, one with limited enrollment one without shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' self.store.update_item(shib_course, '**replace_user**') open_enroll_course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') open_enroll_course.enrollment_domain = '' self.store.update_item(open_enroll_course, '**replace_user**') # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth shib_student = UserFactory.create() shib_student.save() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=shib_student) extauth.save() other_ext_student = UserFactory.create() other_ext_student.username = "******" other_ext_student.email = "*****@*****.**" other_ext_student.save() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://other.edu/', external_credentials="", user=other_ext_student) extauth.save() int_student = UserFactory.create() int_student.username = "******" int_student.email = "*****@*****.**" int_student.save() # Tests the two case for courses, limited and not for course in [shib_course, open_enroll_course]: for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post('/change_enrollment') request.POST.update({'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string()}) request.user = student response = change_enrollment(request) # If course is not limited or student has correct shib extauth then enrollment should be allowed if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up CourseEnrollment.unenroll(student, course.id) else: self.assertEqual(response.status_code, 400) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
def base_extauth_bypass_sending_activation_email( self, bypass_activation_email_for_extauth_setting): """ Tests user creation without sending activation email when doing external auth """ request = self.request_factory.post(self.url, self.params) # now indicate we are doing ext_auth by setting 'ExternalAuthMap' in the session. request.session = import_module( settings.SESSION_ENGINE).SessionStore() # empty session extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='*****@*****.**', internal_password=self.params['password'], external_domain='shib:https://idp.stanford.edu/') request.session['ExternalAuthMap'] = extauth request.user = AnonymousUser() mako_middleware_process_request(request) with mock.patch('django.contrib.auth.models.User.email_user' ) as mock_send_mail: student.views.create_account(request) # check that send_mail is called if bypass_activation_email_for_extauth_setting: self.assertFalse(mock_send_mail.called) else: self.assertTrue(mock_send_mail.called)
def setUp(self): super(ExternalAuthShibTest, self).setUp() self.course = CourseFactory.create( org='Stanford', number='456', display_name='NO SHIB', user_id=self.user.id, ) self.shib_course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.user.id, ) self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save()
def setUp(self): self.store = editable_modulestore() self.course = CourseFactory.create(org='Stanford', number='456', display_name='NO SHIB') self.shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') self.shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' self.store.update_item(self.shib_course, '**replace_user**') self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save()
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", internal_password="******", user=student, ) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org="Stanford", number="123", display_name="Shib Only", enrollment_domain="shib:https://idp.stanford.edu/", user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() params = [ ("course_id", course.id.to_deprecated_string()), ("enrollment_action", "enroll"), ("next", "/testredirect"), ] request_kwargs = { "path": "/shib-login/", "data": dict(params), "follow": False, "REMOTE_USER": "******", "Shib-Identity-Provider": "https://idp.stanford.edu/", } response = self.client.get(**request_kwargs) # successful login is a redirect to the URL that handles auto-enrollment self.assertEqual(response.status_code, 302) self.assertEqual(response["location"], "http://testserver/account/finish_auth?{}".format(urlencode(params)))
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() params = [('course_id', course.id.to_deprecated_string()), ('enrollment_action', 'enroll'), ('next', '/testredirect')] request_kwargs = { 'path': '/shib-login/', 'data': dict(params), 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/' } response = self.client.get(**request_kwargs) # successful login is a redirect to the URL that handles auto-enrollment self.assertEqual(response.status_code, 302) self.assertEqual( response['location'], 'http://testserver/account/finish_auth?{}'.format( urlencode(params)))
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() params = [ ('course_id', course.id.to_deprecated_string()), ('enrollment_action', 'enroll'), ('next', '/testredirect') ] request_kwargs = {'path': '/shib-login/', 'data': dict(params), 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/'} response = self.client.get(**request_kwargs) # successful login is a redirect to the URL that handles auto-enrollment self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/account/finish_auth?{}'.format(urlencode(params)))
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET params """ if not settings.MITX_FEATURES.get('AUTH_USE_SHIB'): return student = UserFactory.create() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') course.enrollment_domain = 'shib:https://idp.stanford.edu/' metadata = own_metadata(course) metadata['enrollment_domain'] = course.enrollment_domain self.store.update_metadata(course.location.url(), metadata) #use django test client for sessions and url processing #no enrollment before trying self.assertEqual(CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 0) self.client.logout() request_kwargs = {'path': '/shib-login/', 'data': {'enrollment_action': 'enroll', 'course_id': course.id}, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/'} response = self.client.get(**request_kwargs) #successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/') #now there is enrollment self.assertEqual(CourseEnrollment.objects.filter(user=student, course_id=course.id).count(), 1)
def external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug('Found eamap=%s' % eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug('Created eamap=%s' % eamap) eamap.save() internal_user = eamap.user if internal_user is None: log.debug('No user for %s yet, doing signup' % eamap.external_email) return signup(request, eamap) uname = internal_user.username user = authenticate(username=uname, password=eamap.internal_password) if user is None: log.warning("External Auth Login failed for %s / %s" % (uname, eamap.internal_password)) return signup(request, eamap) if not user.is_active: log.warning("User %s is not active" % (uname)) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) student_views.try_change_enrollment(request) log.info("Login success - {0} ({1})".format(user.username, user.email)) if retfun is None: return redirect('/') return retfun()
def setUp(self): self.store = modulestore() self.course = CourseFactory.create(org="Stanford", number="456", display_name="NO SHIB") self.shib_course = CourseFactory.create(org="Stanford", number="123", display_name="Shib Only") self.shib_course.enrollment_domain = "shib:https://idp.stanford.edu/" self.store.update_item(self.shib_course, "**replace_user**") self.user_w_map = UserFactory.create(email="*****@*****.**") self.extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="*****@*****.**", external_domain="shib:https://idp.stanford.edu/", external_credentials="", user=self.user_w_map, ) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email="*****@*****.**") self.user_wo_map.save()
def test_authmap_repair(self): """Run authmap check and repair""" self._setstaff_login() Users().create_user('test0', 'test test') # Will raise exception, so no assert needed eamap = ExternalAuthMap.objects.get(external_name='test test') mitu = User.objects.get(username='******') self.assertTrue(check_password(eamap.internal_password, mitu.password)) mitu.set_password('not autogenerated') mitu.save() self.assertFalse(check_password(eamap.internal_password, mitu.password)) # Create really non user AuthMap ExternalAuthMap(external_id='ll', external_domain='ll', external_credentials='{}', external_email='[email protected]', external_name='c', internal_password='').save() response = self.client.post(reverse('sysadmin'), { 'action': 'repair_eamap', }) self.assertIn('{0} test0'.format('Failed in authenticating'), response.content) self.assertIn('fixed password', response.content.decode('utf-8')) self.assertTrue( self.client.login(username='******', password=eamap.internal_password)) # Check for all OK self._setstaff_login() response = self.client.post(reverse('sysadmin'), { 'action': 'repair_eamap', }) self.assertIn('All ok!', response.content.decode('utf-8'))
def test_ext_auth_password_length_too_short(self): """ Tests that even if password policy is enforced, ext_auth registrations aren't subject to it """ self.url_params['password'] = '******' # shouldn't pass validation request = self.request_factory.post(self.url, self.url_params) # now indicate we are doing ext_auth by setting 'ExternalAuthMap' in the session. request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='*****@*****.**', internal_password=self.url_params['password'], external_domain='shib:https://idp.stanford.edu/') request.session['ExternalAuthMap'] = extauth request.user = AnonymousUser() with patch('edxmako.request_context.get_current_request', return_value=request): response = create_account(request) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertTrue(obj['success'])
def external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug('Found eamap=%s', eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug('Created eamap=%s', eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) internal_user = eamap.user if internal_user is None: if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): # if we are using shib, try to link accounts using email try: link_user = User.objects.get(email=eamap.external_email) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info('SHIB: Linking existing account for %s', eamap.external_email) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _(dedent(""" You have already created an account using an external login like WebAuth or Shibboleth. Please contact %s for support """ % getattr(settings, 'TECH_SUPPORT_EMAIL', '*****@*****.**'))) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info('SHIB: No user for %s yet, doing signup', eamap.external_email) return signup(request, eamap) else: log.info('No user for %s yet. doing signup', eamap.external_email) return signup(request, eamap) # We trust shib's authentication, so no need to authenticate using the password again if settings.MITX_FEATURES.get('AUTH_USE_SHIB'): uname = internal_user.username user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'django.contrib.auth.backends.ModelBackend' user.backend = auth_backend log.info('SHIB: Logging in linked user %s', user.email) else: uname = internal_user.username user = authenticate(username=uname, password=eamap.internal_password) if user is None: log.warning("External Auth Login failed for %s / %s", uname, eamap.internal_password) return signup(request, eamap) if not user.is_active: log.warning("User %s is not active", uname) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia if settings.MITX_FEATURES.get('AUTH_USE_SHIB') and 'shib:' in external_domain and request.method == 'GET': enroll_request = make_shib_enrollment_request(request) student_views.try_change_enrollment(enroll_request) else: student_views.try_change_enrollment(request) log.info("Login success - %s (%s)", user.username, user.email) if retfun is None: return redirect('/') return retfun()
def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='*****@*****.**') extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='*****@*****.**') user_w_map.save() user_wo_map.save() extauth.save() inactive_user = UserFactory.create(email='*****@*****.**') inactive_user.is_active = False inactive_extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=inactive_user) inactive_user.save() inactive_extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = [ '*****@*****.**', '*****@*****.**', 'testuser2@someother_idp.com', '*****@*****.**' ] for idp in idps: for remote_user in remote_users: self.client.logout() with patch('external_auth.views.AUDIT_LOG') as mock_audit_log: response = self.client.get( reverse('shib-login'), **{ 'Shib-Identity-Provider': idp, 'mail': remote_user, 'REMOTE_USER': remote_user, }) audit_log_calls = mock_audit_log.method_calls if idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertRedirects(response, '/dashboard') self.assertEquals( int(self.client.session['_auth_user_id']), user_w_map.id) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertEqual(response.status_code, 403) self.assertIn( "Account not yet activated: please look for link in your email", response.content) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'warning') self.assertEquals(len(args), 1) self.assertIn(u'is not active after external login', args[0]) # self.assertEquals(remote_user, args[1]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsNotNone( ExternalAuthMap.objects.get(user=user_wo_map)) self.assertRedirects(response, '/dashboard') self.assertEquals( int(self.client.session['_auth_user_id']), user_wo_map.id) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://someother.idp.com/" and remote_user in \ ['*****@*****.**', '*****@*****.**', '*****@*****.**']: self.assertEqual(response.status_code, 403) self.assertIn( "You have already created an account using an external login", response.content) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) else: self.assertEqual(response.status_code, 200) self.assertContains( response, ("Preferences for {platform_name}".format( platform_name=settings.PLATFORM_NAME))) # no audit logging calls self.assertEquals(len(audit_log_calls), 0)
def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='*****@*****.**') extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='*****@*****.**') user_w_map.save() user_wo_map.save() extauth.save() inactive_user = UserFactory.create(email='*****@*****.**') inactive_user.is_active = False inactive_extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=inactive_user) inactive_user.save() inactive_extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = ['*****@*****.**', '*****@*****.**', 'testuser2@someother_idp.com', '*****@*****.**'] for idp in idps: for remote_user in remote_users: request = self.request_factory.get('/shib-login') request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, 'REMOTE_USER': remote_user, 'mail': remote_user}) request.user = AnonymousUser() with patch('external_auth.views.AUDIT_LOG') as mock_audit_log: response = shib_login(request) audit_log_calls = mock_audit_log.method_calls if idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 3) self.assertIn(u'Login success', args[0]) self.assertEquals(remote_user, args[2]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertEqual(response.status_code, 403) self.assertIn("Account not yet activated: please look for link in your email", response.content) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'warning') self.assertEquals(len(args), 2) self.assertIn(u'is not active after external login', args[0]) # self.assertEquals(remote_user, args[1]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 3) self.assertIn(u'Login success', args[0]) self.assertEquals(remote_user, args[2]) elif idp == "https://someother.idp.com/" and remote_user in \ ['*****@*****.**', '*****@*****.**', '*****@*****.**']: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) else: self.assertEqual(response.status_code, 200) self.assertContains(response, "<title>Register for") # no audit logging calls self.assertEquals(len(audit_log_calls), 0)
class ExternalAuthShibTest(ModuleStoreTestCase): """ Tests how login_user() interacts with ExternalAuth, in particular Shib """ def setUp(self): super(ExternalAuthShibTest, self).setUp() self.course = CourseFactory.create( org='Stanford', number='456', display_name='NO SHIB', user_id=self.user.id, ) self.shib_course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.user.id, ) self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save() @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_page_redirect(self): """ Tests that when a shib user types their email address into the login page, they get redirected to the shib login. """ response = self.client.post(reverse('login'), { 'email': self.user_w_map.email, 'password': '' }) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertEqual(obj, { 'success': False, 'redirect': reverse('shib-login'), }) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_required_dashboard(self): """ Tests redirects to when @login_required to dashboard, which should always be the normal login, since there is no course context """ response = self.client.get(reverse('dashboard')) self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://testserver/login?next=/dashboard') @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_externalauth_login_required_course_context(self): """ Tests the redirects when visiting course-specific URL with @login_required. Should vary by course depending on its enrollment_domain """ TARGET_URL = reverse('courseware', args=[self.course.id.to_deprecated_string()]) # pylint: disable=invalid-name noshib_response = self.client.get(TARGET_URL, follow=True) self.assertEqual( noshib_response.redirect_chain[-1], ('http://testserver/login?next={url}'.format(url=TARGET_URL), 302)) self.assertContains(noshib_response, ("Sign in or Register | {platform_name}".format( platform_name=settings.PLATFORM_NAME))) self.assertEqual(noshib_response.status_code, 200) TARGET_URL_SHIB = reverse( 'courseware', args=[self.shib_course.id.to_deprecated_string()]) # pylint: disable=invalid-name shib_response = self.client.get( **{ 'path': TARGET_URL_SHIB, 'follow': True, 'REMOTE_USER': self.extauth.external_id, 'Shib-Identity-Provider': 'https://idp.stanford.edu/' }) # Test that the shib-login redirect page with ?next= and the desired page are part of the redirect chain # The 'courseware' page actually causes a redirect itself, so it's not the end of the chain and we # won't test its contents self.assertEqual(shib_response.redirect_chain[-3], ('http://testserver/shib-login/?next={url}'.format( url=TARGET_URL_SHIB), 302)) self.assertEqual( shib_response.redirect_chain[-2], ('http://testserver{url}'.format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.status_code, 200)
def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have the proper external auth """ # create 2 course, one with limited enrollment one without shib_course = CourseFactory.create(org="Stanford", number="123", display_name="Shib Only") shib_course.enrollment_domain = "shib:https://idp.stanford.edu/" self.store.update_item(shib_course, "**replace_user**") open_enroll_course = CourseFactory.create(org="MITx", number="999", display_name="Robot Super Course") open_enroll_course.enrollment_domain = "" self.store.update_item(open_enroll_course, "**replace_user**") # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth shib_student = UserFactory.create() shib_student.save() extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", user=shib_student, ) extauth.save() other_ext_student = UserFactory.create() other_ext_student.username = "******" other_ext_student.email = "*****@*****.**" other_ext_student.save() extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://other.edu/", external_credentials="", user=other_ext_student, ) extauth.save() int_student = UserFactory.create() int_student.username = "******" int_student.email = "*****@*****.**" int_student.save() # Tests the two case for courses, limited and not for course in [shib_course, open_enroll_course]: for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post("/change_enrollment") request.POST.update({"enrollment_action": "enroll", "course_id": course.id}) request.user = student response = change_enrollment(request) # If course is not limited or student has correct shib extauth then enrollment should be allowed if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up CourseEnrollment.unenroll(student, course.id) else: self.assertEqual(response.status_code, 400) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug("Found eamap=%s", eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap( external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials) ) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug("Created eamap=%s", eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get("AUTH_USE_SHIB") and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) uses_certs = settings.FEATURES.get("AUTH_USE_CERTIFICATES") internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info("SHIB: Linking existing account for %s", eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _( dedent( """ You have already created an account using an external login like WebAuth or Shibboleth. Please contact %s for support """ % getattr(settings, "TECH_SUPPORT_EMAIL", "*****@*****.**") ) ) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info("SHIB: No user for %s yet, doing signup", eamap.external_email) return _signup(request, eamap, retfun) else: log.info("No user for %s yet. doing signup", eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = "django.contrib.auth.backends.ModelBackend" user.backend = auth_backend if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.info("Linked user.id: {0} logged in via Shibboleth".format(user.id)) else: AUDIT_LOG.info('Linked user "{0}" logged in via Shibboleth'.format(user.email)) elif uses_certs: # Certificates are trusted, so just link the user and log the action user = internal_user user.backend = "django.contrib.auth.backends.ModelBackend" if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.info("Linked user_id {0} logged in via SSL certificate".format(user.id)) else: AUDIT_LOG.info('Linked user "{0}" logged in via SSL certificate'.format(user.email)) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.warning("External Auth Login failed") else: AUDIT_LOG.warning('External Auth Login failed for "{0}"'.format(uname)) return _signup(request, eamap, retfun) if not user.is_active: if settings.FEATURES.get("BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH"): # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users # that aren't already active user.is_active = True user.save() if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.info("Activating user {0} due to external auth".format(user.id)) else: AUDIT_LOG.info('Activating user "{0}" due to external auth'.format(uname)) else: if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.warning("User {0} is not active after external login".format(user.id)) else: AUDIT_LOG.warning('User "{0}" is not active after external login'.format(uname)) # TODO: improve error page msg = "Account not yet activated: please look for link in your email" return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia if uses_shibboleth and request.method == "GET": enroll_request = _make_shib_enrollment_request(request) student.views.try_change_enrollment(enroll_request) else: student.views.try_change_enrollment(request) if settings.FEATURES["SQUELCH_PII_IN_LOGS"]: AUDIT_LOG.info("Login success - user.id: {0}".format(user.id)) else: AUDIT_LOG.info("Login success - {0} ({1})".format(user.username, user.email)) if retfun is None: return redirect("/") return retfun()
class ExternalAuthShibTest(ModuleStoreTestCase): """ Tests how login_user() interacts with ExternalAuth, in particular Shib """ def setUp(self): self.store = editable_modulestore() self.course = CourseFactory.create(org='Stanford', number='456', display_name='NO SHIB') self.shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') self.shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' metadata = own_metadata(self.shib_course) metadata['enrollment_domain'] = self.shib_course.enrollment_domain self.store.update_metadata(self.shib_course.location.url(), metadata) self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save() @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_page_redirect(self): """ Tests that when a shib user types their email address into the login page, they get redirected to the shib login. """ response = self.client.post(reverse('login'), {'email': self.user_w_map.email, 'password': ''}) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, json.dumps({'success': False, 'redirect': reverse('shib-login')})) @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test__get_course_enrollment_domain(self): """ Tests the _get_course_enrollment_domain utility function """ self.assertIsNone(_get_course_enrollment_domain("I/DONT/EXIST")) self.assertIsNone(_get_course_enrollment_domain(self.course.id)) self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id)) @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_required_dashboard(self): """ Tests redirects to when @login_required to dashboard, which should always be the normal login, since there is no course context """ response = self.client.get(reverse('dashboard')) self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://testserver/accounts/login?next=/dashboard') @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_externalauth_login_required_course_context(self): """ Tests the redirects when visiting course-specific URL with @login_required. Should vary by course depending on its enrollment_domain """ TARGET_URL = reverse('courseware', args=[self.course.id]) # pylint: disable=C0103 noshib_response = self.client.get(TARGET_URL, follow=True) self.assertEqual(noshib_response.redirect_chain[-1], ('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302)) self.assertContains(noshib_response, ("<title>Log into your {platform_name} Account</title>" .format(platform_name=settings.PLATFORM_NAME))) self.assertEqual(noshib_response.status_code, 200) TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id]) # pylint: disable=C0103 shib_response = self.client.get(**{'path': TARGET_URL_SHIB, 'follow': True, 'REMOTE_USER': self.extauth.external_id, 'Shib-Identity-Provider': 'https://idp.stanford.edu/'}) # Test that the shib-login redirect page with ?next= and the desired page are part of the redirect chain # The 'courseware' page actually causes a redirect itself, so it's not the end of the chain and we # won't test its contents self.assertEqual(shib_response.redirect_chain[-3], ('http://testserver/shib-login/?next={url}'.format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.redirect_chain[-2], ('http://testserver{url}'.format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.status_code, 200)
def handle(self, *args, **options): while True: uname = raw_input('username: '******'Create MIT ExternalAuth? [n] ').lower() == 'y': email = '*****@*****.**' % uname if not email.endswith('@MIT.EDU'): print "Failed - email must be @MIT.EDU" sys.exit(-1) mit_domain = 'ssl:MIT' if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain): print "Failed - email %s already exists as external_id" % email sys.exit(-1) make_eamap = True password = GenPasswd(12) # get name from kerberos try: kname = os.popen("finger %s | grep 'name:'" % email).read().strip().split('name: ')[1].strip() except: kname = '' name = raw_input('Full name: [%s] ' % kname).strip() if name == '': name = kname print "name = %s" % name else: while True: password = getpass() password2 = getpass() if password == password2: break print "Oops, passwords do not match, please retry" while True: email = raw_input('email: ') if User.objects.filter(email=email): print "email %s already taken" % email else: break name = raw_input('Full name: ') user = User(username=uname, email=email, is_active=True) user.set_password(password) try: user.save() except IntegrityError: print "Oops, failed to create user %s, IntegrityError" % user raise r = Registration() r.register(user) up = UserProfile(user=user) up.name = name up.save() if make_eamap: credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % (name, email) eamap = ExternalAuthMap(external_id=email, external_email=email, external_domain=mit_domain, external_name=name, internal_password=password, external_credentials=json.dumps(credentials), ) eamap.user = user eamap.dtsignup = datetime.datetime.now(UTC) eamap.save() print "User %s created successfully!" % user if not raw_input('Add user %s to any groups? [n] ' % user).lower() == 'y': sys.exit(0) print "Here are the groups available:" groups = [str(g.name) for g in Group.objects.all()] print groups completer = MyCompleter(groups) readline.set_completer(completer.complete) readline.parse_and_bind('tab: complete') while True: gname = raw_input("Add group (tab to autocomplete, empty line to end): ") if not gname: break if not gname in groups: print "Unknown group %s" % gname continue g = Group.objects.get(name=gname) user.groups.add(g) print "Added %s to group %s" % (user, g) print "Done!"
def create_user(self, uname, name, password=None): """ Creates a user (both SSL and regular)""" if not uname: return _('Must provide username') if not name: return _('Must provide full name') email_domain = getattr(settings, 'SSL_AUTH_EMAIL_DOMAIN', 'MIT.EDU') msg = u'' if settings.FEATURES['AUTH_USE_CERTIFICATES']: if not '@' in uname: email = '{0}@{1}'.format(uname, email_domain) else: email = uname if not email.endswith('@{0}'.format(email_domain)): msg += u'{0} @{1}'.format(_('email must end in'), email_domain) return msg mit_domain = 'ssl:MIT' if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain): msg += _('Failed - email {0} already exists as ' 'external_id').format(email) return msg new_password = generate_password() else: if not password: return _('Password must be supplied if not using certificates') email = uname if not '@' in email: msg += _('email address required (not username)') return msg new_password = password user = User(username=uname, email=email, is_active=True) user.set_password(new_password) try: user.save() except IntegrityError: msg += _('Oops, failed to create user {0}, ' 'IntegrityError').format(user) return msg reg = Registration() reg.register(user) profile = UserProfile(user=user) profile.name = name profile.save() if settings.FEATURES['AUTH_USE_CERTIFICATES']: credential_string = getattr(settings, 'SSL_AUTH_DN_FORMAT_STRING', '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}') credentials = credential_string.format(name, email) eamap = ExternalAuthMap( external_id=email, external_email=email, external_domain=mit_domain, external_name=name, internal_password=new_password, external_credentials=json.dumps(credentials), ) eamap.user = user eamap.dtsignup = timezone.now() eamap.save() msg += _('User {0} created successfully!').format(user) return msg
class ExternalAuthShibTest(ModuleStoreTestCase): """ Tests how login_user() interacts with ExternalAuth, in particular Shib """ def setUp(self): super(ExternalAuthShibTest, self).setUp() self.course = CourseFactory.create( org='Stanford', number='456', display_name='NO SHIB', user_id=self.user.id, ) self.shib_course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.user.id, ) self.user_w_map = UserFactory.create(email='*****@*****.**') self.extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='*****@*****.**', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=self.user_w_map) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email='*****@*****.**') self.user_wo_map.save() @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_page_redirect(self): """ Tests that when a shib user types their email address into the login page, they get redirected to the shib login. """ response = self.client.post(reverse('login'), {'email': self.user_w_map.email, 'password': ''}) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertEqual(obj, { 'success': False, 'redirect': reverse('shib-login'), }) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test__get_course_enrollment_domain(self): """ Tests the _get_course_enrollment_domain utility function """ self.assertIsNone(_get_course_enrollment_domain(SlashSeparatedCourseKey("I", "DONT", "EXIST"))) self.assertIsNone(_get_course_enrollment_domain(self.course.id)) self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id)) @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_login_required_dashboard(self): """ Tests redirects to when @login_required to dashboard, which should always be the normal login, since there is no course context """ response = self.client.get(reverse('dashboard')) self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://testserver/accounts/login?next=/dashboard') @unittest.skipUnless(settings.FEATURES.get('AUTH_USE_SHIB'), "AUTH_USE_SHIB not set") def test_externalauth_login_required_course_context(self): """ Tests the redirects when visiting course-specific URL with @login_required. Should vary by course depending on its enrollment_domain """ TARGET_URL = reverse('courseware', args=[self.course.id.to_deprecated_string()]) # pylint: disable=C0103 noshib_response = self.client.get(TARGET_URL, follow=True) self.assertEqual(noshib_response.redirect_chain[-1], ('http://testserver/accounts/login?next={url}'.format(url=TARGET_URL), 302)) self.assertContains(noshib_response, ("Log into your {platform_name} Account | {platform_name}" .format(platform_name=settings.PLATFORM_NAME))) self.assertEqual(noshib_response.status_code, 200) TARGET_URL_SHIB = reverse('courseware', args=[self.shib_course.id.to_deprecated_string()]) # pylint: disable=C0103 shib_response = self.client.get(**{'path': TARGET_URL_SHIB, 'follow': True, 'REMOTE_USER': self.extauth.external_id, 'Shib-Identity-Provider': 'https://idp.stanford.edu/'}) # Test that the shib-login redirect page with ?next= and the desired page are part of the redirect chain # The 'courseware' page actually causes a redirect itself, so it's not the end of the chain and we # won't test its contents self.assertEqual(shib_response.redirect_chain[-3], ('http://testserver/shib-login/?next={url}'.format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.redirect_chain[-2], ('http://testserver{url}'.format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.status_code, 200)
def handle(self, *args, **options): while True: uname = raw_input('username: '******'Create MIT ExternalAuth? [n] ').lower() == 'y': email = '*****@*****.**' % uname if not email.endswith('@MIT.EDU'): print "Failed - email must be @MIT.EDU" sys.exit(-1) mit_domain = 'ssl:MIT' if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain): print "Failed - email %s already exists as external_id" % email sys.exit(-1) make_eamap = True password = GenPasswd(12) # get name from kerberos try: kname = os.popen( "finger %s | grep 'name:'" % email).read().strip().split('name: ')[1].strip() except: kname = '' name = raw_input('Full name: [%s] ' % kname).strip() if name == '': name = kname print "name = %s" % name else: while True: password = getpass() password2 = getpass() if password == password2: break print "Oops, passwords do not match, please retry" while True: email = raw_input('email: ') if User.objects.filter(email=email): print "email %s already taken" % email else: break name = raw_input('Full name: ') user = User(username=uname, email=email, is_active=True) user.set_password(password) try: user.save() except IntegrityError: print "Oops, failed to create user %s, IntegrityError" % user raise r = Registration() r.register(user) up = UserProfile(user=user) up.name = name up.save() if make_eamap: credentials = "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN=%s/emailAddress=%s" % ( name, email) eamap = ExternalAuthMap( external_id=email, external_email=email, external_domain=mit_domain, external_name=name, internal_password=password, external_credentials=json.dumps(credentials), ) eamap.user = user eamap.dtsignup = datetime.datetime.now(UTC) eamap.save() print "User %s created successfully!" % user if not raw_input( 'Add user %s to any groups? [n] ' % user).lower() == 'y': sys.exit(0) print "Here are the groups available:" groups = [str(g.name) for g in Group.objects.all()] print groups completer = MyCompleter(groups) readline.set_completer(completer.complete) readline.parse_and_bind('tab: complete') while True: gname = raw_input( "Add group (tab to autocomplete, empty line to end): ") if not gname: break if gname not in groups: print "Unknown group %s" % gname continue g = Group.objects.get(name=gname) user.groups.add(g) print "Added %s to group %s" % (user, g) print "Done!"
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" logout(request) # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug('Found eamap=%s', eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug('Created eamap=%s', eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.MITX_FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info('SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _(dedent(""" You have already created an account using an external login like WebAuth or Shibboleth. Please contact %s for support """ % getattr(settings, 'TECH_SUPPORT_EMAIL', '*****@*****.**'))) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info('SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap) else: log.info('No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'django.contrib.auth.backends.ModelBackend' user.backend = auth_backend AUDIT_LOG.info('Linked user "%s" logged in via Shibboleth', user.email) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: AUDIT_LOG.warning('External Auth Login failed for "%s"', uname) return _signup(request, eamap) if not user.is_active: AUDIT_LOG.warning('User "%s" is not active after external login', uname) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia if uses_shibboleth and request.method == 'GET': enroll_request = _make_shib_enrollment_request(request) student.views.try_change_enrollment(enroll_request) else: student.views.try_change_enrollment(request) AUDIT_LOG.info("Login success - %s (%s)", user.username, user.email) if retfun is None: return redirect('/') return retfun()
class ExternalAuthShibTest(ModuleStoreTestCase): """ Tests how login_user() interacts with ExternalAuth, in particular Shib """ def setUp(self): self.store = modulestore() self.course = CourseFactory.create(org="Stanford", number="456", display_name="NO SHIB") self.shib_course = CourseFactory.create(org="Stanford", number="123", display_name="Shib Only") self.shib_course.enrollment_domain = "shib:https://idp.stanford.edu/" self.store.update_item(self.shib_course, "**replace_user**") self.user_w_map = UserFactory.create(email="*****@*****.**") self.extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="*****@*****.**", external_domain="shib:https://idp.stanford.edu/", external_credentials="", user=self.user_w_map, ) self.user_w_map.save() self.extauth.save() self.user_wo_map = UserFactory.create(email="*****@*****.**") self.user_wo_map.save() @unittest.skipUnless(settings.FEATURES.get("AUTH_USE_SHIB"), "AUTH_USE_SHIB not set") def test_login_page_redirect(self): """ Tests that when a shib user types their email address into the login page, they get redirected to the shib login. """ response = self.client.post(reverse("login"), {"email": self.user_w_map.email, "password": ""}) self.assertEqual(response.status_code, 200) obj = json.loads(response.content) self.assertEqual(obj, {"success": False, "redirect": reverse("shib-login")}) @unittest.skipUnless(settings.FEATURES.get("AUTH_USE_SHIB"), "AUTH_USE_SHIB not set") def test__get_course_enrollment_domain(self): """ Tests the _get_course_enrollment_domain utility function """ self.assertIsNone(_get_course_enrollment_domain(SlashSeparatedCourseKey("I", "DONT", "EXIST"))) self.assertIsNone(_get_course_enrollment_domain(self.course.id)) self.assertEqual(self.shib_course.enrollment_domain, _get_course_enrollment_domain(self.shib_course.id)) @unittest.skipUnless(settings.FEATURES.get("AUTH_USE_SHIB"), "AUTH_USE_SHIB not set") def test_login_required_dashboard(self): """ Tests redirects to when @login_required to dashboard, which should always be the normal login, since there is no course context """ response = self.client.get(reverse("dashboard")) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "http://testserver/accounts/login?next=/dashboard") @unittest.skipUnless(settings.FEATURES.get("AUTH_USE_SHIB"), "AUTH_USE_SHIB not set") def test_externalauth_login_required_course_context(self): """ Tests the redirects when visiting course-specific URL with @login_required. Should vary by course depending on its enrollment_domain """ TARGET_URL = reverse("courseware", args=[self.course.id.to_deprecated_string()]) # pylint: disable=C0103 noshib_response = self.client.get(TARGET_URL, follow=True) self.assertEqual( noshib_response.redirect_chain[-1], ("http://testserver/accounts/login?next={url}".format(url=TARGET_URL), 302), ) self.assertContains( noshib_response, ("Log into your {platform_name} Account | {platform_name}".format(platform_name=settings.PLATFORM_NAME)), ) self.assertEqual(noshib_response.status_code, 200) TARGET_URL_SHIB = reverse( "courseware", args=[self.shib_course.id.to_deprecated_string()] ) # pylint: disable=C0103 shib_response = self.client.get( **{ "path": TARGET_URL_SHIB, "follow": True, "REMOTE_USER": self.extauth.external_id, "Shib-Identity-Provider": "https://idp.stanford.edu/", } ) # Test that the shib-login redirect page with ?next= and the desired page are part of the redirect chain # The 'courseware' page actually causes a redirect itself, so it's not the end of the chain and we # won't test its contents self.assertEqual( shib_response.redirect_chain[-3], ("http://testserver/shib-login/?next={url}".format(url=TARGET_URL_SHIB), 302), ) self.assertEqual(shib_response.redirect_chain[-2], ("http://testserver{url}".format(url=TARGET_URL_SHIB), 302)) self.assertEqual(shib_response.status_code, 200)
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug(u'Found eamap=%s', eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug(u'Created eamap=%s', eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get( 'AUTH_USE_SHIB') and external_domain.startswith( SHIBBOLETH_DOMAIN_PREFIX) uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES') internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info(u'SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _( "You have already created an account using " "an external login like WebAuth or Shibboleth. " "Please contact {tech_support_email} for support." ).format(tech_support_email=settings.TECH_SUPPORT_EMAIL, ) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap, retfun) else: log.info(u'No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend' user.backend = auth_backend if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Linked user.id: {0} logged in via Shibboleth'.format( user.id)) else: AUDIT_LOG.info( u'Linked user "{0}" logged in via Shibboleth'.format( user.email)) elif uses_certs: # Certificates are trusted, so just link the user and log the action user = internal_user user.backend = 'ratelimitbackend.backends.RateLimitModelBackend' if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Linked user_id {0} logged in via SSL certificate'.format( user.id)) else: AUDIT_LOG.info( u'Linked user "{0}" logged in via SSL certificate'.format( user.email)) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'External Auth Login failed') else: AUDIT_LOG.warning( u'External Auth Login failed for "{0}"'.format(uname)) return _signup(request, eamap, retfun) if not user.is_active: if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users # that aren't already active user.is_active = True user.save() if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info( u'Activating user {0} due to external auth'.format( user.id)) else: AUDIT_LOG.info( u'Activating user "{0}" due to external auth'.format( uname)) else: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning( u'User {0} is not active after external login'.format( user.id)) else: AUDIT_LOG.warning( u'User "{0}" is not active after external login'.format( uname)) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id)) else: AUDIT_LOG.info(u"Login success - {0} ({1})".format( user.username, user.email)) if retfun is None: return redirect('/') return retfun()
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug('Found eamap=%s', eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug('Created eamap=%s', eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info('SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _(dedent(""" You have already created an account using an external login like WebAuth or Shibboleth. Please contact %s for support """ % getattr(settings, 'TECH_SUPPORT_EMAIL', '*****@*****.**'))) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info('SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap, retfun) else: log.info('No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'django.contrib.auth.backends.ModelBackend' user.backend = auth_backend AUDIT_LOG.info('Linked user "%s" logged in via Shibboleth', user.email) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: AUDIT_LOG.warning('External Auth Login failed for "%s"', uname) return _signup(request, eamap, retfun) if not user.is_active: AUDIT_LOG.warning('User "%s" is not active after external login', uname) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) # Now to try enrollment # Need to special case Shibboleth here because it logs in via a GET. # testing request.method for extra paranoia if uses_shibboleth and request.method == 'GET': enroll_request = _make_shib_enrollment_request(request) student.views.try_change_enrollment(enroll_request) else: student.views.try_change_enrollment(request) AUDIT_LOG.info("Login success - %s (%s)", user.username, user.email) if retfun is None: return redirect('/') return retfun()
def create_user(self, uname, name, password=None): """ Creates a user (both SSL and regular)""" if not uname: return _("Must provide username") if not name: return _("Must provide full name") email_domain = getattr(settings, "SSL_AUTH_EMAIL_DOMAIN", "MIT.EDU") msg = u"" if settings.FEATURES["AUTH_USE_CERTIFICATES"]: if "@" not in uname: email = "{0}@{1}".format(uname, email_domain) else: email = uname if not email.endswith("@{0}".format(email_domain)): # Translators: Domain is an email domain, such as "@gmail.com" msg += _("Email address must end in {domain}").format(domain="@{0}".format(email_domain)) return msg mit_domain = "ssl:MIT" if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain): msg += _("Failed - email {email_addr} already exists as {external_id}").format( email_addr=email, external_id="external_id" ) return msg new_password = generate_password() else: if not password: return _("Password must be supplied if not using certificates") email = uname if "@" not in email: msg += _("email address required (not username)") return msg new_password = password user = User(username=uname, email=email, is_active=True) user.set_password(new_password) try: user.save() except IntegrityError: msg += _("Oops, failed to create user {user}, {error}").format(user=user, error="IntegrityError") return msg reg = Registration() reg.register(user) profile = UserProfile(user=user) profile.name = name profile.save() if settings.FEATURES["AUTH_USE_CERTIFICATES"]: credential_string = getattr( settings, "SSL_AUTH_DN_FORMAT_STRING", "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}", ) credentials = credential_string.format(name, email) eamap = ExternalAuthMap( external_id=email, external_email=email, external_domain=mit_domain, external_name=name, internal_password=new_password, external_credentials=json.dumps(credentials), ) eamap.user = user eamap.dtsignup = timezone.now() eamap.save() msg += _("User {user} created successfully!").format(user=user) return msg
def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email='*****@*****.**') extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=user_w_map) user_wo_map = UserFactory.create(email='*****@*****.**') user_w_map.save() user_wo_map.save() extauth.save() inactive_user = UserFactory.create(email='*****@*****.**') inactive_user.is_active = False inactive_extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=inactive_user) inactive_user.save() inactive_extauth.save() idps = ['https://idp.stanford.edu/', 'https://someother.idp.com/'] remote_users = ['*****@*****.**', '*****@*****.**', 'testuser2@someother_idp.com', '*****@*****.**'] for idp in idps: for remote_user in remote_users: request = self.request_factory.get('/shib-login') request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({'Shib-Identity-Provider': idp, 'REMOTE_USER': remote_user, 'mail': remote_user}) request.user = AnonymousUser() mako_middleware_process_request(request) with patch('external_auth.views.AUDIT_LOG') as mock_audit_log: response = shib_login(request) audit_log_calls = mock_audit_log.method_calls if idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response['Location'], '/dashboard') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertEqual(response.status_code, 403) self.assertIn("Account not yet activated: please look for link in your email", response.content) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'warning') self.assertEquals(len(args), 1) self.assertIn(u'is not active after external login', args[0]) # self.assertEquals(remote_user, args[1]) elif idp == "https://idp.stanford.edu/" and remote_user == '*****@*****.**': self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response['Location'], '/dashboard') # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, 'info') self.assertEquals(len(args), 1) self.assertIn(u'Login success', args[0]) self.assertIn(remote_user, args[0]) elif idp == "https://someother.idp.com/" and remote_user in \ ['*****@*****.**', '*****@*****.**', '*****@*****.**']: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) else: self.assertEqual(response.status_code, 200) self.assertContains(response, ("Preferences for {platform_name}" .format(platform_name=settings.PLATFORM_NAME))) # no audit logging calls self.assertEquals(len(audit_log_calls), 0)
def create_user(self, uname, name, password=None): """ Creates a user (both SSL and regular)""" if not uname: return _('Must provide username') if not name: return _('Must provide full name') email_domain = getattr(settings, 'SSL_AUTH_EMAIL_DOMAIN', 'MIT.EDU') msg = u'' if settings.FEATURES['AUTH_USE_CERTIFICATES']: if not '@' in uname: email = '{0}@{1}'.format(uname, email_domain) else: email = uname if not email.endswith('@{0}'.format(email_domain)): msg += u'{0} @{1}'.format(_('email must end in'), email_domain) return msg mit_domain = 'ssl:MIT' if ExternalAuthMap.objects.filter(external_id=email, external_domain=mit_domain): msg += _('Failed - email {0} already exists as ' 'external_id').format(email) return msg new_password = generate_password() else: if not password: return _('Password must be supplied if not using certificates') email = uname if not '@' in email: msg += _('email address required (not username)') return msg new_password = password user = User(username=uname, email=email, is_active=True) user.set_password(new_password) try: user.save() except IntegrityError: msg += _('Oops, failed to create user {0}, ' 'IntegrityError').format(user) return msg reg = Registration() reg.register(user) profile = UserProfile(user=user) profile.name = name profile.save() if settings.FEATURES['AUTH_USE_CERTIFICATES']: credential_string = getattr( settings, 'SSL_AUTH_DN_FORMAT_STRING', '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}' ) credentials = credential_string.format(name, email) eamap = ExternalAuthMap( external_id=email, external_email=email, external_domain=mit_domain, external_name=name, internal_password=new_password, external_credentials=json.dumps(credentials), ) eamap.user = user eamap.dtsignup = timezone.now() eamap.save() msg += _('User {0} created successfully!').format(user) return msg
def _external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, retfun=None): """Generic external auth login or signup""" # see if we have a map from this external_id to an edX username try: eamap = ExternalAuthMap.objects.get(external_id=external_id, external_domain=external_domain) log.debug(u'Found eamap=%s', eamap) except ExternalAuthMap.DoesNotExist: # go render form for creating edX user eamap = ExternalAuthMap(external_id=external_id, external_domain=external_domain, external_credentials=json.dumps(credentials)) eamap.external_email = email eamap.external_name = fullname eamap.internal_password = generate_password() log.debug(u'Created eamap=%s', eamap) eamap.save() log.info(u"External_Auth login_or_signup for %s : %s : %s : %s", external_domain, external_id, email, fullname) uses_shibboleth = settings.FEATURES.get('AUTH_USE_SHIB') and external_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) uses_certs = settings.FEATURES.get('AUTH_USE_CERTIFICATES') internal_user = eamap.user if internal_user is None: if uses_shibboleth: # If we are using shib, try to link accounts # For Stanford shib, the email the idp returns is actually under the control of the user. # Since the id the idps return is not user-editable, and is of the from "*****@*****.**", # use the id to link accounts instead. try: link_user = User.objects.get(email=eamap.external_id) if not ExternalAuthMap.objects.filter(user=link_user).exists(): # if there's no pre-existing linked eamap, we link the user eamap.user = link_user eamap.save() internal_user = link_user log.info(u'SHIB: Linking existing account for %s', eamap.external_id) # now pass through to log in else: # otherwise, there must have been an error, b/c we've already linked a user with these external # creds failure_msg = _( "You have already created an account using " "an external login like WebAuth or Shibboleth. " "Please contact {tech_support_email} for support." ).format( tech_support_email=settings.TECH_SUPPORT_EMAIL, ) return default_render_failure(request, failure_msg) except User.DoesNotExist: log.info(u'SHIB: No user for %s yet, doing signup', eamap.external_email) return _signup(request, eamap, retfun) else: log.info(u'No user for %s yet. doing signup', eamap.external_email) return _signup(request, eamap, retfun) # We trust shib's authentication, so no need to authenticate using the password again uname = internal_user.username if uses_shibboleth: user = internal_user # Assuming this 'AUTHENTICATION_BACKENDS' is set in settings, which I think is safe if settings.AUTHENTICATION_BACKENDS: auth_backend = settings.AUTHENTICATION_BACKENDS[0] else: auth_backend = 'ratelimitbackend.backends.RateLimitModelBackend' user.backend = auth_backend if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Linked user.id: {0} logged in via Shibboleth'.format(user.id)) else: AUDIT_LOG.info(u'Linked user "{0}" logged in via Shibboleth'.format(user.email)) elif uses_certs: # Certificates are trusted, so just link the user and log the action user = internal_user user.backend = 'ratelimitbackend.backends.RateLimitModelBackend' if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Linked user_id {0} logged in via SSL certificate'.format(user.id)) else: AUDIT_LOG.info(u'Linked user "{0}" logged in via SSL certificate'.format(user.email)) else: user = authenticate(username=uname, password=eamap.internal_password, request=request) if user is None: # we want to log the failure, but don't want to log the password attempted: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'External Auth Login failed') else: AUDIT_LOG.warning(u'External Auth Login failed for "{0}"'.format(uname)) return _signup(request, eamap, retfun) if not user.is_active: if settings.FEATURES.get('BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'): # if BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH, we trust external auth and activate any users # that aren't already active user.is_active = True user.save() if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u'Activating user {0} due to external auth'.format(user.id)) else: AUDIT_LOG.info(u'Activating user "{0}" due to external auth'.format(uname)) else: if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.warning(u'User {0} is not active after external login'.format(user.id)) else: AUDIT_LOG.warning(u'User "{0}" is not active after external login'.format(uname)) # TODO: improve error page msg = 'Account not yet activated: please look for link in your email' return default_render_failure(request, msg) login(request, user) request.session.set_expiry(0) if settings.FEATURES['SQUELCH_PII_IN_LOGS']: AUDIT_LOG.info(u"Login success - user.id: {0}".format(user.id)) else: AUDIT_LOG.info(u"Login success - {0} ({1})".format(user.username, user.email)) if retfun is None: return redirect('/') return retfun()
def test_shib_login(self): """ Tests that: * shib credentials that match an existing ExternalAuthMap with a linked active user logs the user in * shib credentials that match an existing ExternalAuthMap with a linked inactive user shows error page * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user without an existing ExternalAuthMap links the two and log the user in * shib credentials that match an existing ExternalAuthMap without a linked user and also match the email of an existing user that already has an ExternalAuthMap causes an error (403) * shib credentials that do not match an existing ExternalAuthMap causes the registration form to appear """ user_w_map = UserFactory.create(email="*****@*****.**") extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", user=user_w_map, ) user_wo_map = UserFactory.create(email="*****@*****.**") user_w_map.save() user_wo_map.save() extauth.save() inactive_user = UserFactory.create(email="*****@*****.**") inactive_user.is_active = False inactive_extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", user=inactive_user, ) inactive_user.save() inactive_extauth.save() idps = ["https://idp.stanford.edu/", "https://someother.idp.com/"] remote_users = [ "*****@*****.**", "*****@*****.**", "testuser2@someother_idp.com", "*****@*****.**", ] for idp in idps: for remote_user in remote_users: request = self.request_factory.get("/shib-login") request.session = import_module(settings.SESSION_ENGINE).SessionStore() # empty session request.META.update({"Shib-Identity-Provider": idp, "REMOTE_USER": remote_user, "mail": remote_user}) request.user = AnonymousUser() with patch("external_auth.views.AUDIT_LOG") as mock_audit_log: response = shib_login(request) audit_log_calls = mock_audit_log.method_calls if idp == "https://idp.stanford.edu/" and remote_user == "*****@*****.**": self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_w_map) self.assertEqual(response["Location"], "/") # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, "info") self.assertEquals(len(args), 3) self.assertIn(u"Login success", args[0]) self.assertEquals(remote_user, args[2]) elif idp == "https://idp.stanford.edu/" and remote_user == "*****@*****.**": self.assertEqual(response.status_code, 403) self.assertIn("Account not yet activated: please look for link in your email", response.content) # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, "warning") self.assertEquals(len(args), 2) self.assertIn(u"is not active after external login", args[0]) # self.assertEquals(remote_user, args[1]) elif idp == "https://idp.stanford.edu/" and remote_user == "*****@*****.**": self.assertIsNotNone(ExternalAuthMap.objects.get(user=user_wo_map)) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(request.user, user_wo_map) self.assertEqual(response["Location"], "/") # verify logging: self.assertEquals(len(audit_log_calls), 2) self._assert_shib_login_is_logged(audit_log_calls[0], remote_user) method_name, args, _kwargs = audit_log_calls[1] self.assertEquals(method_name, "info") self.assertEquals(len(args), 3) self.assertIn(u"Login success", args[0]) self.assertEquals(remote_user, args[2]) elif idp == "https://someother.idp.com/" and remote_user in [ "*****@*****.**", "*****@*****.**", "*****@*****.**", ]: self.assertEqual(response.status_code, 403) self.assertIn("You have already created an account using an external login", response.content) # no audit logging calls self.assertEquals(len(audit_log_calls), 0) else: self.assertEqual(response.status_code, 200) self.assertContains( response, ("<title>Preferences for {platform_name}</title>".format(platform_name=settings.PLATFORM_NAME)), ) # no audit logging calls self.assertEquals(len(audit_log_calls), 0)