def test_start_flow(self): # Request temporary credentials from provider, provide auth redirect httpretty.register_uri(httpretty.POST, 'http://mock1a.com/request', body='{"oauth_token_secret": "temp_secret", ' '"oauth_token": "temp_token", ' '"oauth_callback_confirmed": "true"}', status=200, content_type='application/json') with self.app.app.test_request_context('/oauth/connect/mock1a/'): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # auth_url is a property method - it calls out to the external # service to get a temporary key and secret before returning the # auth url url = self.provider.auth_url # The URL to which the user would be redirected assert_equal(url, "http://mock1a.com/auth?oauth_token=temp_token") session = get_session() # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_equal(creds['token'], 'temp_token') assert_equal(creds['secret'], 'temp_secret')
def test_start_flow(self): # Request temporary credentials from provider, provide auth redirect httpretty.register_uri(httpretty.POST, 'http://mock1a.com/request', body='{"oauth_token_secret": "temp_secret", ' '"oauth_token": "temp_token", ' '"oauth_callback_confirmed": "true"}', status=200, content_type='application/json') with self.app.app.test_request_context('/oauth/connect/mock1a/'): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # auth_url is a property method - it calls out to the external # service to get a temporary key and secret before returning the # auth url url = self.provider.auth_url # The URL to which the user would be redirected assert_equal(url, "http://mock1a.com/auth?oauth_token=temp_token") # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_equal(creds['token'], 'temp_token') assert_equal(creds['secret'], 'temp_secret')
def test_callback(self): # Exchange temporary credentials for permanent credentials # Mock the exchange of the code for an access token _prepare_mock_oauth2_handshake_response() user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state" ): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) session = get_session() session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange self.provider.auth_callback(user=user) account = ExternalAccount.find_one() assert_equal(account.oauth_key, 'mock_access_token') assert_equal(account.provider_id, 'mock_provider_id')
def test_provider_down(self): # Create a 500 error _prepare_mock_500_error() user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state" ): # make sure the user is logged in authenticate(user=user, access_token=None, response=None) session = get_session() session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange with assert_raises(HTTPError) as error_raised: self.provider.auth_callback(user=user) assert_equal( error_raised.exception.code, 503, )
def test_callback(self): # Exchange temporary credentials for permanent credentials # Mock the exchange of the code for an access token _prepare_mock_oauth2_handshake_response() user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state"): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange self.provider.auth_callback(user=user) account = ExternalAccount.find_one() assert_equal(account.oauth_key, 'mock_access_token') assert_equal(account.provider_id, 'mock_provider_id')
def test_provider_down(self): # Create a 500 error _prepare_mock_500_error() user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state"): # make sure the user is logged in authenticate(user=user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange with assert_raises(HTTPError) as error_raised: self.provider.auth_callback(user=user) assert_equal( error_raised.exception.code, 503, )
def test_callback_oauth_no_redirect(self, mock_fetch_token, mock_oauth2session): # During token exchange, OAuth2Session is initialize w/o redirect_uri for non-standard ones. # Temporarily add the mock provider to the `ADDONS_OAUTH_NO_REDIRECT` list. ADDONS_OAUTH_NO_REDIRECT.append(self.provider.short_name) # Mock OAuth2Session and its property `fetch_token`. mock_oauth2session.return_value = OAuth2Session( self.provider.client_id, None) mock_fetch_token.return_value = {'access_token': 'mock_access_token'} user = UserFactory() with self.app.app.test_request_context( path='/oauth/callback/mock2/', query_string='code=mock_code&state=mock_state'): authenticate(user=self.user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state' } } session.save() self.provider.auth_callback(user=user) mock_oauth2session.assert_called_with(self.provider.client_id, redirect_uri=None) # Reset the `ADDONS_OAUTH_NO_REDIRECT` list. ADDONS_OAUTH_NO_REDIRECT.remove(self.provider.short_name)
def test_callback_oauth_standard(self, mock_fetch_token, mock_oauth2session): # During token exchange, OAuth2Session is initialized w/ redirect_uri for standard addons. # Make sure that the mock oauth2 provider is a standard one. assert self.provider.short_name not in ADDONS_OAUTH_NO_REDIRECT # Mock OAuth2Session and its property `fetch_token`. mock_oauth2session.return_value = OAuth2Session( self.provider.client_id, None) mock_fetch_token.return_value = {'access_token': 'mock_access_token'} user = UserFactory() with self.app.app.test_request_context( path='/oauth/callback/mock2/', query_string='code=mock_code&state=mock_state'): authenticate(user=self.user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state' } } session.save() self.provider.auth_callback(user=user) redirect_uri = web_url_for('oauth_callback', service_name=self.provider.short_name, _absolute=True) mock_oauth2session.assert_called_with(self.provider.client_id, redirect_uri=redirect_uri)
def test_multiple_users_associated(self): # Create only one ExternalAccount for multiple OSF users # # For some providers (ex: GitHub), the act of completing the OAuth flow # revokes previously generated credentials. In addition, there is often no # way to know the user's id on the external service until after the flow # has completed. # # Having only one ExternalAccount instance per account on the external # service means that connecting subsequent OSF users to the same external # account will not invalidate the credentials used by the OSF for users # already associated. user_a = UserFactory() external_account = ExternalAccountFactory( provider='mock2', provider_id='mock_provider_id', provider_name='Mock Provider', ) user_a.external_accounts.append(external_account) user_a.save() user_b = UserFactory() # Mock the exchange of the code for an access token _prepare_mock_oauth2_handshake_response() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state" ) as ctx: # make sure the user is logged in authenticate(user=user_b, access_token=None, response=None) session = get_session() session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange self.provider.auth_callback(user=user_b) user_a.reload() user_b.reload() external_account.reload() assert_equal( user_a.external_accounts, user_b.external_accounts, ) assert_equal( ExternalAccount.find().count(), 1 )
def test_multiple_users_associated(self): # Create only one ExternalAccount for multiple OSF users # # For some providers (ex: GitHub), the act of completing the OAuth flow # revokes previously generated credentials. In addition, there is often no # way to know the user's id on the external service until after the flow # has completed. # # Having only one ExternalAccount instance per account on the external # service means that connecting subsequent OSF users to the same external # account will not invalidate the credentials used by the OSF for users # already associated. user_a = UserFactory() external_account = ExternalAccountFactory( provider='mock2', provider_id='mock_provider_id', provider_name='Mock Provider', ) user_a.external_accounts.add(external_account) user_a.save() user_b = UserFactory() # Mock the exchange of the code for an access token _prepare_mock_oauth2_handshake_response() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="code=mock_code&state=mock_state" ) as ctx: # make sure the user is logged in authenticate(user=user_b, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() # do the key exchange self.provider.auth_callback(user=user_b) user_a.reload() user_b.reload() external_account.reload() assert_equal( list(user_a.external_accounts.values_list('pk', flat=True)), list(user_b.external_accounts.values_list('pk', flat=True)), ) assert_equal( ExternalAccount.find().count(), 1 )
def test_callback_wrong_user(self): # Reject temporary credentials not assigned to the user # # This prohibits users from associating their external account with # another user's OSF account by using XSS or similar attack vector to # complete the OAuth flow using the logged-in user but their own account # on the external service. # # If the OSF were to allow login via OAuth with the provider in question, # this would allow attackers to hijack OSF accounts with a simple script # injection. # mock a successful call to the provider to exchange temp keys for # permanent keys responses.add( responses.Response( responses.POST, 'http://mock1a.com/callback', body='oauth_token=perm_token' '&oauth_token_secret=perm_secret' '&oauth_callback_confirmed=true', ) ) user = UserFactory() account = ExternalAccountFactory( provider="mock1a", provider_name='Mock 1A', oauth_key="temp_key", oauth_secret="temp_secret" ) account.save() # associate this ExternalAccount instance with the user user.external_accounts.add(account) user.save() malicious_user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock1a/", query_string="oauth_token=temp_key&oauth_verifier=mock_verifier" ): # make sure the user is logged in authenticate(user=malicious_user, access_token=None, response=None) with assert_raises(PermissionsError): # do the key exchange self.provider.auth_callback(user=malicious_user)
def test_callback_wrong_user(self): # Reject temporary credentials not assigned to the user # # This prohibits users from associating their external account with # another user's OSF account by using XSS or similar attack vector to # complete the OAuth flow using the logged-in user but their own account # on the external service. # # If the OSF were to allow login via OAuth with the provider in question, # this would allow attackers to hijack OSF accounts with a simple script # injection. # mock a successful call to the provider to exchange temp keys for # permanent keys responses.add( responses.Response( responses.POST, 'http://mock1a.com/callback', body='oauth_token=perm_token' '&oauth_token_secret=perm_secret' '&oauth_callback_confirmed=true', ) ) user = UserFactory() account = ExternalAccountFactory( provider='mock1a', provider_name='Mock 1A', oauth_key='temp_key', oauth_secret='temp_secret' ) account.save() # associate this ExternalAccount instance with the user user.external_accounts.add(account) user.save() malicious_user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path='/oauth/callback/mock1a/', query_string='oauth_token=temp_key&oauth_verifier=mock_verifier' ): # make sure the user is logged in authenticate(user=malicious_user, access_token=None, response=None) with assert_raises(PermissionsError): # do the key exchange self.provider.auth_callback(user=malicious_user)
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) # `service_url` is guaranteed to be removed of `ticket` parameter, which has been pulled off in # `framework.sessions.before_request()`. if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() return redirect( get_logout_url( get_login_url(service_url, username=user.username, verification_key=user.verification_key))) # if user is authenticated by CAS return authenticate(user, cas_resp.attributes['accessToken'], redirect(service_furl.url)) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = u'{} {}'.format( cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() if not fullname: fullname = external_credential['id'] user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes['accessToken'], 'service_url': service_furl.url, } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get'))) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() return redirect(get_logout_url(get_login_url( service_url, username=user.username, verification_key=user.verification_key ))) # if user is authenticated by CAS return authenticate( user, cas_resp.attributes['accessToken'], redirect(service_furl.url) ) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = '{} {}'.format(cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() if not fullname: fullname = external_credential['id'] user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes['accessToken'], } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get')) ) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def before_request(): from framework.auth import authenticate from framework.auth.core import User from framework.auth import cas # Central Authentication Server Ticket Validation and Authentication ticket = request.args.get('ticket') if ticket: service_url = furl.furl(request.url) service_url.args.pop('ticket') # Attempt autn wih CAS, and return a proper redirect response return cas.make_response_from_ticket(ticket=ticket, service_url=service_url.url) # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) except cas.CasTokenError as err: # NOTE: We assume that the request is an AJAX request return jsonify({'message_short': 'Invalid Bearer token', 'message_long': err.args[0]}), http.UNAUTHORIZED cas_resp = client.profile(access_token) if cas_resp.authenticated: user = User.load(cas_resp.user) return authenticate(user, access_token=access_token, response=None) return make_response('', http.UNAUTHORIZED) if request.authorization: # TODO: Fix circular import from framework.auth.core import get_user user = get_user( email=request.authorization.username, password=request.authorization.password ) # Create empty session # TODO: Shoudn't need to create a session for Basic Auth session = Session() if user: session.data['auth_user_username'] = user.username session.data['auth_user_id'] = user._primary_key session.data['auth_user_fullname'] = user.fullname else: # Invalid key: Not found in database session.data['auth_error_code'] = http.FORBIDDEN set_session(session) return cookie = request.cookies.get(settings.COOKIE_NAME) if cookie: try: session_id = itsdangerous.Signer(settings.SECRET_KEY).unsign(cookie) session = Session.load(session_id) or Session(_id=session_id) set_session(session) return except: pass
def test_callback(self): # Exchange temporary credentials for permanent credentials # mock a successful call to the provider to exchange temp keys for # permanent keys responses.add( responses.Response( responses.POST, 'http://mock1a.com/callback', body=( 'oauth_token=perm_token' '&oauth_token_secret=perm_secret' '&oauth_callback_confirmed=true' ) ) ) user = UserFactory() # Fake a request context for the callback ctx = self.app.app.test_request_context( path='/oauth/callback/mock1a/', query_string='oauth_token=temp_key&oauth_verifier=mock_verifier', ) with ctx: # make sure the user is logged in authenticate(user=user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'token': 'temp_key', 'secret': 'temp_secret', }, } session.save() # do the key exchange self.provider.auth_callback(user=user) account = ExternalAccount.objects.first() assert_equal(account.oauth_key, 'perm_token') assert_equal(account.oauth_secret, 'perm_secret') assert_equal(account.provider_id, 'mock_provider_id') assert_equal(account.provider_name, 'Mock OAuth 1.0a Provider')
def test_start_flow(self): # Generate the appropriate URL and state token with self.app.app.test_request_context("/oauth/connect/mock2/"): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # auth_url is a property method - it calls out to the external # service to get a temporary key and secret before returning the # auth url url = self.provider.auth_url session = get_session() # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_in('state', creds) # The URL to which the user would be redirected parsed = urlparse.urlparse(url) params = urlparse.parse_qs(parsed.query) # check parameters assert_equal( params, { 'state': [creds['state']], 'response_type': ['code'], 'client_id': [self.provider.client_id], 'redirect_uri': [ web_url_for('oauth_callback', service_name=self.provider.short_name, _absolute=True) ] } ) # check base URL assert_equal( url.split("?")[0], "https://mock2.com/auth", )
def test_start_flow(self): # Generate the appropriate URL and state token with self.app.app.test_request_context("/oauth/connect/mock2/"): # make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # auth_url is a property method - it calls out to the external # service to get a temporary key and secret before returning the # auth url url = self.provider.auth_url # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_in('state', creds) # The URL to which the user would be redirected parsed = urlparse.urlparse(url) params = urlparse.parse_qs(parsed.query) # check parameters assert_equal( params, { 'state': [creds['state']], 'response_type': ['code'], 'client_id': [self.provider.client_id], 'redirect_uri': [ web_url_for('oauth_callback', service_name=self.provider.short_name, _absolute=True) ] } ) # check base URL assert_equal( url.split("?")[0], "https://mock2.com/auth", )
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() if external_credential: user.verification_key = generate_verification_key() user.save() return redirect( get_logout_url( get_login_url(service_url, username=user.username, verification_key=user.verification_key))) return authenticate(user, cas_resp.attributes['accessToken'], redirect(service_furl.url)) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = '{} {}'.format( cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() if not fullname: fullname = external_credential['id'] user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes['accessToken'], } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get'))) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def test_start_flow_oauth_standard(self): # Generate the appropriate URL and state token - addons that follow standard OAuth protocol # Make sure that the mock oauth2 provider is a standard one. The test would fail early here # if `test_start_flow_oauth_no_redirect()` was ran before this test and failed in the middle # without resetting the `ADDONS_OAUTH_NO_REDIRECT` list. assert self.provider.short_name not in ADDONS_OAUTH_NO_REDIRECT with self.app.app.test_request_context('/oauth/connect/mock2/'): # Make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # `auth_url` is a property method - it calls out to the external service to get a # temporary key and secret before returning the auth url url = self.provider.auth_url # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_in('state', creds) # The URL to which the user would be redirected parsed = urlparse(url) params = parse_qs(parsed.query) # Check parameters expected_params = { 'state': [creds['state']], 'response_type': ['code'], 'client_id': [self.provider.client_id], 'redirect_uri': [ web_url_for('oauth_callback', service_name=self.provider.short_name, _absolute=True) ] } assert_equal(params, expected_params) # Check base URL assert_equal(url.split('?')[0], 'https://mock2.com/auth')
def test_user_denies_access(self): # Create a 401 error _prepare_mock_401_error() user = UserFactory() # Fake a request context for the callback with self.app.app.test_request_context( path="/oauth/callback/mock2/", query_string="error=mock_error&code=mock_code&state=mock_state" ): # make sure the user is logged in authenticate(user=user, access_token=None, response=None) session.data['oauth_states'] = { self.provider.short_name: { 'state': 'mock_state', }, } session.save() assert_false(self.provider.auth_callback(user=user))
def claim_user_form(auth, **kwargs): """View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect( web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) user = User.load(uid) # The unregistered user # user ID is invalid. Unregistered user is not in database if not user: raise HTTPError(http.BAD_REQUEST) # If claim token not valid, redirect to registration page if not verify_claim_token(user, token, pid): return redirect('/account/') unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() email = unclaimed_record['email'] form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if form.validate(): username, password = form.username.data, form.password.data user.register(username=username, password=password) # Clear unclaimed records user.unclaimed_records = {} user.save() # Authenticate user and redirect to project page response = redirect('/settings/') node = Node.load(pid) status.push_status_message( language.CLAIMED_CONTRIBUTOR.format(node=node), 'success') return authenticate(user, response) else: forms.push_errors_to_status(form.errors) return { 'firstname': user.given_name, 'email': email if email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, }
def test_start_flow_oauth_no_redirect(self): # Generate the appropriate URL and state token - addons that do not allow `redirect_uri` # Temporarily add the mock provider to the `ADDONS_OAUTH_NO_REDIRECT` list ADDONS_OAUTH_NO_REDIRECT.append(self.provider.short_name) with self.app.app.test_request_context('/oauth/connect/mock2/'): # Make sure the user is logged in authenticate(user=self.user, access_token=None, response=None) # `auth_url` is a property method - it calls out to the external service to get a # temporary key and secret before returning the auth url url = self.provider.auth_url # Temporary credentials are added to the session creds = session.data['oauth_states'][self.provider.short_name] assert_in('state', creds) # The URL to which the user would be redirected parsed = urlparse(url) params = parse_qs(parsed.query) # Check parameters - the only difference from standard OAuth flow is no `redirect_uri`. expected_params = { 'state': [creds['state']], 'response_type': ['code'], 'client_id': [self.provider.client_id] } assert_equal(params, expected_params) # Check base URL assert_equal(url.split('?')[0], 'https://mock2.com/auth') # Reset the `ADDONS_OAUTH_NO_REDIRECT` list ADDONS_OAUTH_NO_REDIRECT.remove(self.provider.short_name)
def claim_user_form(auth, **kwargs): """View for rendering the set password page for a claimed user. Must have ``token`` as a querystring argument. Renders the set password form, validates it, and sets the user's password. """ uid, pid = kwargs['uid'], kwargs['pid'] token = request.form.get('token') or request.args.get('token') # If user is logged in, redirect to 're-enter password' page if auth.logged_in: return redirect(web_url_for('claim_user_registered', uid=uid, pid=pid, token=token)) user = User.load(uid) # The unregistered user # user ID is invalid. Unregistered user is not in database if not user: raise HTTPError(http.BAD_REQUEST) # If claim token not valid, redirect to registration page if not verify_claim_token(user, token, pid): return redirect('/account/') unclaimed_record = user.unclaimed_records[pid] user.fullname = unclaimed_record['name'] user.update_guessed_names() email = unclaimed_record['email'] form = SetEmailAndPasswordForm(request.form, token=token) if request.method == 'POST': if form.validate(): username, password = email, form.password.data user.register(username=username, password=password) # Clear unclaimed records user.unclaimed_records = {} user.save() # Authenticate user and redirect to project page response = redirect('/settings/') node = Node.load(pid) status.push_status_message(language.CLAIMED_CONTRIBUTOR.format(node=node), 'success') return authenticate(user, response) else: forms.push_errors_to_status(form.errors) return { 'firstname': user.given_name, 'email': email if email else '', 'fullname': user.fullname, 'form': forms.utils.jsonify(form) if is_json_request() else form, }
def post(self, request, *args, **kwargs): error = '' data = request.POST employee_id = data.get('employee_id') password = data.get('password') user = auth.authenticate(employee_id=employee_id, password=password) if user: auth.login(request, user) return HttpResponseRedirect(reverse('framework:index')) else: employee_id = employee_id password = '' error = True return render(request, 'framework/login.html', { 'employee_id': employee_id, 'error': error })
def make_response_from_ticket(ticket, service_url): """Given a CAS ticket and service URL, attempt to the user and return a proper redirect response. """ service_furl = furl.furl(service_url) if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user = User.load(cas_resp.user) # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() return authenticate(user, access_token=cas_resp.attributes['accessToken'], response=redirect(service_furl.url)) # Ticket could not be validated, unauthorized. return redirect(service_furl.url)
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) # user found and authenticated if user and action == 'authenticate': # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user.verification_key = None user.save() return authenticate( user, cas_resp.attributes['accessToken'], redirect(service_furl.url) ) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': from website.util import web_url_for # TODO: [#OSF-6935] verify both names are in attributes, which should be handled in CAS user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': '{} {}'.format(cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')), 'access_token': cas_resp.attributes['accessToken'], } return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get')) ) # Unauthorized: ticket could not be validated, or user does not exist. return redirect(service_furl.url)
def confirm_email_get(**kwargs): """View for email confirmation links. Authenticates and redirects to user settings page if confirmation is successful, otherwise shows an "Expired Link" error. methods: GET """ user = User.load(kwargs['uid']) token = kwargs['token'] if user: if user.confirm_email(token): # Confirm and register the usre user.date_last_login = datetime.datetime.utcnow() user.save() # Go to settings page status.push_status_message(language.WELCOME_MESSAGE, 'success') response = redirect('/settings/') return auth.authenticate(user, response=response) # Return data for the error template return { 'code': http.BAD_REQUEST, 'message_short': 'Link Expired', 'message_long': language.LINK_EXPIRED }, http.BAD_REQUEST
def before_request(): from framework.auth import authenticate from framework.auth.core import User from framework.auth import cas # Central Authentication Server Ticket Validation and Authentication ticket = request.args.get('ticket') if ticket: service_url = furl.furl(request.url) service_url.args.pop('ticket') # Attempt autn wih CAS, and return a proper redirect response return cas.make_response_from_ticket(ticket=ticket, service_url=service_url.url) # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) except cas.CasTokenError as err: # NOTE: We assume that the request is an AJAX request return jsonify({'message_short': 'Invalid Bearer token', 'message_long': err.args[0]}), http.UNAUTHORIZED cas_resp = client.profile(access_token) if cas_resp.authenticated: user = User.load(cas_resp.user) return authenticate(user, access_token=access_token, response=None) return make_response('', http.UNAUTHORIZED) if request.authorization: # Create a session from the API key; if key is # not valid, save the HTTP error code in the # "auth_error_code" field of session.data # Create empty session session = Session() # Hack: Avoid circular import from website.project.model import ApiKey api_label = request.authorization.username api_key_id = request.authorization.password api_key = ApiKey.load(api_key_id) if api_key: user = api_key.user__keyed and api_key.user__keyed[0] node = api_key.node__keyed and api_key.node__keyed[0] session.data['auth_api_label'] = api_label session.data['auth_api_key'] = api_key._primary_key if user: session.data['auth_user_username'] = user.username session.data['auth_user_id'] = user._primary_key session.data['auth_user_fullname'] = user.fullname elif node: session.data['auth_node_id'] = node._primary_key else: # Invalid key: Not attached to user or node session.data['auth_error_code'] = http.FORBIDDEN else: # Invalid key: Not found in database session.data['auth_error_code'] = http.FORBIDDEN set_session(session) return cookie = request.cookies.get(settings.COOKIE_NAME) if cookie: try: session_id = itsdangerous.Signer(settings.SECRET_KEY).unsign(cookie) session = Session.load(session_id) or Session(_id=session_id) set_session(session) return except: pass ## TODO: Create session in before_request, cookie in after_request ## Retry request, preserving status code #response = redirect(request.path, code=307) return create_session(None)
def before_request(): from framework.auth import authenticate from framework.auth.core import User from framework.auth import cas # Central Authentication Server Ticket Validation and Authentication ticket = request.args.get('ticket') if ticket: service_url = furl.furl(request.url) service_url.args.pop('ticket') # Attempt autn wih CAS, and return a proper redirect response return cas.make_response_from_ticket(ticket=ticket, service_url=service_url.url) # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) except cas.CasTokenError as err: # NOTE: We assume that the request is an AJAX request return jsonify({ 'message_short': 'Invalid Bearer token', 'message_long': err.args[0] }), http.UNAUTHORIZED cas_resp = client.profile(access_token) if cas_resp.authenticated: user = User.load(cas_resp.user) return authenticate(user, access_token=access_token, response=None) return make_response('', http.UNAUTHORIZED) if request.authorization: # Create a session from the API key; if key is # not valid, save the HTTP error code in the # "auth_error_code" field of session.data # Create empty session session = Session() # Hack: Avoid circular import from website.project.model import ApiKey api_label = request.authorization.username api_key_id = request.authorization.password api_key = ApiKey.load(api_key_id) if api_key: user = api_key.user__keyed and api_key.user__keyed[0] node = api_key.node__keyed and api_key.node__keyed[0] session.data['auth_api_label'] = api_label session.data['auth_api_key'] = api_key._primary_key if user: session.data['auth_user_username'] = user.username session.data['auth_user_id'] = user._primary_key session.data['auth_user_fullname'] = user.fullname elif node: session.data['auth_node_id'] = node._primary_key else: # Invalid key: Not attached to user or node session.data['auth_error_code'] = http.FORBIDDEN else: # Invalid key: Not found in database session.data['auth_error_code'] = http.FORBIDDEN set_session(session) return cookie = request.cookies.get(settings.COOKIE_NAME) if cookie: try: session_id = itsdangerous.Signer( settings.SECRET_KEY).unsign(cookie) session = Session.load(session_id) or Session(_id=session_id) set_session(session) return except: pass ## TODO: Create session in before_request, cookie in after_request ## Retry request, preserving status code #response = redirect(request.path, code=307) return create_session(None)
def make_response_from_ticket(ticket, service_url): """ Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response. :param str ticket: CAS service ticket :param str service_url: Service URL from which the authentication request originates :return: redirect response """ service_furl = furl.furl(service_url) # `service_url` is guaranteed to be removed of `ticket` parameter, which has been pulled off in # `framework.sessions.before_request()`. if 'ticket' in service_furl.args: service_furl.args.pop('ticket') client = get_client() cas_resp = client.service_validate(ticket, service_furl.url) if cas_resp.authenticated: user, external_credential, action = get_user_from_cas_resp(cas_resp) user_updates = {} # serialize updates to user to be applied async # user found and authenticated if user and action == 'authenticate': print_cas_log( f'CAS response - authenticating user: user=[{user._id}], ' f'external=[{external_credential}], action=[{action}]', LogLevel.INFO, ) # If users check the TOS consent checkbox via CAS, CAS sets the attribute `termsOfServiceChecked` to `true` # and then release it to OSF among other authentication attributes. When OSF receives it, it trusts CAS and # updates the user object if this is THE FINAL STEP of the login flow. DON'T update TOS consent status when # `external_credential == true` (i.e. w/ `action == 'authenticate'` or `action == 'external_first_login'`) # since neither is the final step of a login flow. tos_checked_via_cas = cas_resp.attributes.get( 'termsOfServiceChecked', 'false') == 'true' if tos_checked_via_cas: user_updates['accepted_terms_of_service'] = timezone.now() print_cas_log( f'CAS TOS consent checked: {user.guids.first()._id}, {user.username}', LogLevel.INFO) # if we successfully authenticate and a verification key is present, invalidate it if user.verification_key: user_updates['verification_key'] = None # if user is authenticated by external IDP, ask CAS to authenticate user for a second time # this extra step will guarantee that 2FA are enforced # current CAS session created by external login must be cleared first before authentication if external_credential: user.verification_key = generate_verification_key() user.save() print_cas_log( f'CAS response - redirect existing external IdP login to verification key login: user=[{user._id}]', LogLevel.INFO) return redirect( get_logout_url( get_login_url(service_url, username=user.username, verification_key=user.verification_key))) # if user is authenticated by CAS # TODO [CAS-27]: Remove Access Token From Service Validation print_cas_log( f'CAS response - finalizing authentication: user=[{user._id}]', LogLevel.INFO) return authenticate(user, cas_resp.attributes.get('accessToken', ''), redirect(service_furl.url), user_updates) # first time login from external identity provider if not user and external_credential and action == 'external_first_login': print_cas_log( f'CAS response - first login from external IdP: ' f'external=[{external_credential}], action=[{action}]', LogLevel.INFO, ) from website.util import web_url_for # orcid attributes can be marked private and not shared, default to orcid otherwise fullname = u'{} {}'.format( cas_resp.attributes.get('given-names', ''), cas_resp.attributes.get('family-name', '')).strip() # TODO [CAS-27]: Remove Access Token From Service Validation user = { 'external_id_provider': external_credential['provider'], 'external_id': external_credential['id'], 'fullname': fullname, 'access_token': cas_resp.attributes.get('accessToken', ''), 'service_url': service_furl.url, } print_cas_log( f'CAS response - creating anonymous session: external=[{external_credential}]', LogLevel.INFO) return external_first_login_authenticate( user, redirect(web_url_for('external_login_email_get'))) # Unauthorized: ticket could not be validated, or user does not exist. print_cas_log( 'Ticket validation failed or user does not exist. Redirect back to service URL (logged out).', LogLevel.ERROR) return redirect(service_furl.url)
def before_request(): from framework import sentry from framework.auth import cas from framework.auth.core import User from framework.auth import authenticate from framework.routing import json_renderer # Central Authentication Server Ticket Validation and Authentication ticket = request.args.get('ticket') if ticket: service_url = furl.furl(request.url) service_url.args.pop('ticket') # Attempt autn wih CAS, and return a proper redirect response return cas.make_response_from_ticket(ticket=ticket, service_url=service_url.url) # Central Authentication Server OAuth Bearer Token authorization = request.headers.get('Authorization') if authorization and authorization.startswith('Bearer '): client = cas.get_client() try: access_token = cas.parse_auth_header(authorization) cas_resp = client.profile(access_token) except cas.CasError as err: sentry.log_exception() # NOTE: We assume that the request is an AJAX request return json_renderer(err) if cas_resp.authenticated: user = User.load(cas_resp.user) return authenticate(user, access_token=access_token, response=None) return make_response('', http.UNAUTHORIZED) if request.authorization: # TODO: Fix circular import from framework.auth.core import get_user user = get_user(email=request.authorization.username, password=request.authorization.password) # Create empty session # TODO: Shoudn't need to create a session for Basic Auth session = Session() if user: session.data['auth_user_username'] = user.username session.data['auth_user_id'] = user._primary_key session.data['auth_user_fullname'] = user.fullname else: # Invalid key: Not found in database session.data['auth_error_code'] = http.FORBIDDEN set_session(session) return cookie = request.cookies.get(settings.COOKIE_NAME) if cookie: try: session_id = itsdangerous.Signer( settings.SECRET_KEY).unsign(cookie) session = Session.load(session_id) or Session(_id=session_id) set_session(session) return except: pass