import flask import pytest from flask import url_for from flask_webtest import TestApp as WebtestApp from atlas_um import consts from atlas_um.consts import SystemEvents from atlas_um.tokens.bearer_tokens import DNABearerToken from atlas_um.pgdb import pgdb from tests.atlas_um.factories import DNAAccountFactory from tests.atlas_um.pytest_helpers import get_cookie from tests.atlas_um.pytest_helpers import UrlForThisMixin class TestUSMLogout(UrlForThisMixin): ENDPOINT = "common.userinfo" FAKE_HOST = "atlas_um.test" FAKE_AUTH_PROXY_DOMAIN = f"auth.{FAKE_HOST}" AUTH_PROXY_PARAM = f"https://{FAKE_AUTH_PROXY_DOMAIN}/process" @pytest.fixture def app_config(self): def inner(app): app.config["SERVER_NAME"] = self.FAKE_HOST app.config["ALLOWED_HOSTS"] = [self.FAKE_HOST] return inner @pytest.fixture def client(self, app): return WebtestApp(app) def test_logout_no_bearer_token(self, app): with app.test_request_context(self.url_for_this(app)): expected_url = flask.url_for("common.userinfo") req = url_for("usm_logout.get_logout") with app.test_client() as client: resp = client.get(req) assert resp.status_code == 302 assert resp.location == expected_url def test_logout_invalid_bearer_token(self, app, faker): with app.test_request_context(self.url_for_this(app)): expected_url = flask.url_for("common.userinfo") bad_bearer_token = faker.pystr() req = url_for("usm_logout.get_logout") with app.test_client() as client: client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), bad_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get(req) assert resp.status_code == 302 assert resp.location == expected_url def test_logout_missing_usm_id_token(self, app, pgdb_session): with app.test_request_context(self.url_for_this(app)): expected_url = flask.url_for("common.userinfo") dna_account = DNAAccountFactory.build() pgdb.session.add(dna_account) fake_bearer_token = DNABearerToken(dna_account).encode() req = url_for("usm_logout.get_logout") with app.test_client() as client: client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), fake_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get(req) assert resp.status_code == 302 assert resp.location == expected_url def test_logout_valid_bearer_token(self, app, mocker, faker, pgdb_session): fake_id_token = faker.pystr() mocked_generate_state = mocker.patch( "atlas_um.usm.client.USMClient.generate_state" ) fake_state = faker.pystr() mocked_generate_state.return_value = fake_state mocked_logout_url = mocker.patch( "atlas_um.usm.client.USMClient.logout_url" ) fake_logout_url = faker.uri() mocked_logout_url.return_value = fake_logout_url dna_account = DNAAccountFactory.build() pgdb.session.add(dna_account) fake_bearer_token = DNABearerToken(dna_account).encode() req = url_for("usm_logout.get_logout") with app.test_client() as client: client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), fake_bearer_token, domain=self.FAKE_HOST, httponly=True, ) client.set_cookie( consts.USM_ID_TOKEN_COOKIE_NAME, fake_id_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get(req) session = flask.session.copy() assert resp.status_code == 302 assert resp.location == fake_logout_url assert session[consts.STATE_SESSION_NAME] == fake_state assert mocked_generate_state.call_args_list == [()] assert mocked_logout_url.call_args_list == [ mocker.call(fake_id_token, fake_state) ] # logout_callback def test_logout_callback_no_dna_bearer_token(self, app, faker): fake_state = faker.pystr() with app.test_request_context(self.url_for_this(app)): expected_url = flask.url_for("common.userinfo") with app.test_client() as client: with client.session_transaction() as session: session[consts.STATE_SESSION_NAME] = fake_state resp = client.get( url_for("usm_logout.get_logout_callback", state=fake_state) ) assert resp.status_code == 302 assert resp.location == expected_url def test_logout_callback_bad_dna_bearer_token(self, app, faker): fake_state = faker.pystr() with app.test_request_context(self.url_for_this(app)): expected_url = flask.url_for("common.userinfo") bad_bearer_token = faker.pystr() with app.test_client() as client: with client.session_transaction() as session: session[consts.STATE_SESSION_NAME] = fake_state client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), bad_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get( url_for("usm_logout.get_logout_callback", state=fake_state) ) assert resp.status_code == 302 assert resp.location == expected_url def test_logout_callback_no_state_arg(self, app, pgdb_session): dna_account = DNAAccountFactory.build() dna_bearer_token = DNABearerToken(dna_account).encode() req = url_for("usm_logout.get_logout_callback") with app.test_client() as client: client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), dna_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get(req) assert resp.status_code == 400 def test_logout_callback_no_state_in_session( self, app, faker, pgdb_session ): dna_account = DNAAccountFactory.build() dna_bearer_token = DNABearerToken(dna_account).encode() fake_state = faker.pystr() req = url_for("usm_logout.get_logout_callback", state=fake_state) with app.test_client() as client: client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), dna_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = client.get(req) assert resp.status_code == 400 def test_logout_callback_mismatched_states(self, app, faker, pgdb_session): dna_account = DNAAccountFactory.build() dna_bearer_token = DNABearerToken(dna_account).encode() fake_state_param = faker.pystr() req = url_for("usm_logout.get_logout_callback", state=fake_state_param) fake_session_state = faker.pystr() while fake_session_state == fake_state_param: fake_session_state = faker.pystr() with app.test_client() as other_client: with other_client.session_transaction() as session: session[consts.STATE_SESSION_NAME] = fake_session_state other_client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), dna_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = other_client.get(req) assert resp.status_code == 400 def test_logout_callback_without_auth_proxy( self, app, client, pgdb_session, faker, mocker ): fake_id_token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqYXNvbi53aGl0dGxl" "LnNtZUBzb255bXVzaWMuY29tIiwiYXVkIjoiUzNnaktuSTBVbTQxYUVkNVVWZG9hM" "mh0U2xjIiwiYXV0aF90aW1lIjoxNTk3OTQ0OTI5LCJpc3MiOiJodHRwczovL3VhdC" "11c20uc21lYW5hbHl0aWNzcG9ydGFsLmNvbS9vYXV0aDIvb3BlbmlkIiwicHJlZmV" "ycmVkX3VzZXJuYW1lIjoiamFzb24ud2hpdHRsZS5zbWVAc29ueW11c2ljLmNvbSIs" "ImdpdmVuX25hbWUiOiJKYXNvbiIsImV4cCI6MTU5Nzk0NjEyOSwibm9uY2UiOiJaY" "kRhSkxwUyIsImZhbWlseV9uYW1lIjoiV2hpdHRsZSIsImlhdCI6MTU5Nzk0NDkyOS" "wiZW1haWwiOiJqYXNvbi53aGl0dGxlLnNtZUBzb255bXVzaWMuY29tIiwiY2lkIjo" "iUzNnaktuSTBVbTQxYUVkNVVWZG9hMmh0U2xjIn0=.77+977+9A8uFfu+/vRvvv70" "S77+9UO+/vSx777+9aFsJ77+9Q39677+977+977+977+936fvv73vv70=" ) dna_account = DNAAccountFactory.build() fake_state = faker.pystr() with app.test_request_context(self.url_for_this(app)): pgdb.session.add(dna_account) fake_bearer_token = DNABearerToken(dna_account).encode() req = url_for("usm_logout.get_logout_callback", state=fake_state) mocker.patch( "tldextract.extract" ).return_value.registered_domain = self.FAKE_HOST mocked_user_events = mocker.patch( "atlas_um.analytics.collectors.Collector.log" ) with app.test_client() as other_client: with other_client.session_transaction() as session: session[consts.STATE_SESSION_NAME] = fake_state other_client.set_cookie( consts.USM_ID_TOKEN_COOKIE_NAME, fake_id_token, domain=self.FAKE_HOST, httponly=True, ) other_client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), fake_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = other_client.get(req) assert resp.status_code == 302 assert ( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME") not in client.cookies ) assert consts.USM_ID_TOKEN_COOKIE_NAME not in client.cookies id_cookie = get_cookie(resp, consts.USM_ID_TOKEN_COOKIE_NAME) assert id_cookie is not None assert "HttpOnly" in id_cookie assert id_cookie.get("Domain") == self.FAKE_HOST assert id_cookie.get("Expires") == "Thu, 01 Jan 1970 00:00:00 GMT" assert "Secure" in id_cookie bearer_cookie = get_cookie( resp, app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME") ) assert bearer_cookie is not None assert "HttpOnly" in bearer_cookie assert "Secure" in bearer_cookie assert bearer_cookie.get("Domain") == self.FAKE_HOST assert bearer_cookie.get("Expires") == ( "Thu, 01 Jan 1970 00:00:00 GMT" ) refresh_cookie = get_cookie( resp, app.config.get("DNA_REFRESH_TOKEN_COOKIE_NAME") ) assert refresh_cookie is not None assert "HttpOnly" in refresh_cookie assert "Secure" in refresh_cookie assert refresh_cookie.get("Domain") == self.FAKE_HOST assert refresh_cookie.get("Expires") == ( "Thu, 01 Jan 1970 00:00:00 GMT" ) assert mocked_user_events.call_args_list == [ mocker.call( SystemEvents.successful_logout, domain=self.FAKE_HOST, user_id=dna_account.sub, ) ] with app.app_context(): assert resp.location == url_for("common.userinfo") def test_logout_callback_with_auth_proxy( self, app, client, pgdb_session, faker, mocker ): fake_id_token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqYXNvbi53aGl0dGxl" "LnNtZUBzb255bXVzaWMuY29tIiwiYXVkIjoiUzNnaktuSTBVbTQxYUVkNVVWZG9hM" "mh0U2xjIiwiYXV0aF90aW1lIjoxNTk3OTQ0OTI5LCJpc3MiOiJodHRwczovL3VhdC" "11c20uc21lYW5hbHl0aWNzcG9ydGFsLmNvbS9vYXV0aDIvb3BlbmlkIiwicHJlZmV" "ycmVkX3VzZXJuYW1lIjoiamFzb24ud2hpdHRsZS5zbWVAc29ueW11c2ljLmNvbSIs" "ImdpdmVuX25hbWUiOiJKYXNvbiIsImV4cCI6MTU5Nzk0NjEyOSwibm9uY2UiOiJaY" "kRhSkxwUyIsImZhbWlseV9uYW1lIjoiV2hpdHRsZSIsImlhdCI6MTU5Nzk0NDkyOS" "wiZW1haWwiOiJqYXNvbi53aGl0dGxlLnNtZUBzb255bXVzaWMuY29tIiwiY2lkIjo" "iUzNnaktuSTBVbTQxYUVkNVVWZG9hMmh0U2xjIn0=.77+977+9A8uFfu+/vRvvv70" "S77+9UO+/vSx777+9aFsJ77+9Q39677+977+977+977+936fvv73vv70=" ) dna_account = DNAAccountFactory.build() fake_state = faker.pystr() with app.app_context(): pgdb.session.add(dna_account) fake_bearer_token = DNABearerToken(dna_account).encode() req = url_for("usm_logout.get_logout_callback", state=fake_state) mocker.patch( "tldextract.extract" ).return_value.registered_domain = self.FAKE_HOST mocked_user_events = mocker.patch( "atlas_um.analytics.collectors.Collector.log" ) with app.test_client() as other_client: with other_client.session_transaction() as session: session[consts.STATE_SESSION_NAME] = fake_state session[consts.AUTH_PROXY_SESSION_NAME] = self.AUTH_PROXY_PARAM other_client.set_cookie( consts.USM_ID_TOKEN_COOKIE_NAME, fake_id_token, domain=self.FAKE_HOST, httponly=True, ) other_client.set_cookie( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME"), fake_bearer_token, domain=self.FAKE_HOST, httponly=True, ) resp = other_client.get(req) assert resp.status_code == 200 assert self.AUTH_PROXY_PARAM in resp.data.decode() assert ( app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME") not in client.cookies ) assert consts.USM_ID_TOKEN_COOKIE_NAME not in client.cookies id_cookie = get_cookie(resp, consts.USM_ID_TOKEN_COOKIE_NAME) assert id_cookie is not None assert "HttpOnly" in id_cookie assert id_cookie.get("Domain") == self.FAKE_HOST assert id_cookie.get("Expires") == "Thu, 01 Jan 1970 00:00:00 GMT" assert "Secure" in id_cookie bearer_cookie = get_cookie( resp, app.config.get("DNA_BEARER_TOKEN_COOKIE_NAME") ) assert bearer_cookie is not None assert "HttpOnly" in bearer_cookie assert "Secure" in bearer_cookie assert bearer_cookie.get("Domain") == self.FAKE_HOST assert bearer_cookie.get("Expires") == "Thu, 01 Jan 1970 00:00:00 GMT" refresh_cookie = get_cookie( resp, app.config.get("DNA_REFRESH_TOKEN_COOKIE_NAME") ) assert refresh_cookie is not None assert "HttpOnly" in refresh_cookie assert "Secure" in refresh_cookie assert refresh_cookie.get("Domain") == self.FAKE_HOST assert refresh_cookie.get("Expires") == "Thu, 01 Jan 1970 00:00:00 GMT" assert mocked_user_events.call_args_list == [ mocker.call( SystemEvents.successful_logout, domain=self.FAKE_HOST, user_id=dna_account.sub, ) ]