diff --git a/.changes/next-release/enhancement-sso-4703.json b/.changes/next-release/enhancement-sso-4703.json
new file mode 100644
index 000000000000..d4b4dacfb0be
--- /dev/null
+++ b/.changes/next-release/enhancement-sso-4703.json
@@ -0,0 +1,5 @@
+{
+ "type": "enhancement",
+ "category": "sso",
+ "description": "The browser tab opened by ``aws sso login`` for the Authorization Code flow now makes a best-effort attempt to close itself after a successful login. This is a no-op in browsers that block it (e.g. after an interactive login), where the page continues to display a message that the tab can be closed."
+}
diff --git a/awscli/customizations/sso/index.html b/awscli/customizations/sso/index.html
index 1c2a3b8dfe5b..988de537c826 100644
--- a/awscli/customizations/sso/index.html
+++ b/awscli/customizations/sso/index.html
@@ -121,8 +121,8 @@
Sign in to AWS
- Your credentials have been shared successfully and can be used until your session expires. You can
- now close this tab.
+ Your credentials have been shared successfully and can be used until your session expires. This tab
+ will try to close automatically—if it is still open, you can safely close it now.
@@ -146,6 +146,12 @@
Sign in to AWS
document.getElementById('success-message').classList.add('hidden');
document.getElementById('error-message').classList.remove('hidden');
document.getElementById('error-text').innerText = error;
+ } else {
+ // Best-effort tab close. Browsers only allow window.close() on
+ // tabs opened by script or with a single history entry, so this
+ // is a no-op after an interactive login. When it is blocked the
+ // success message above tells the user to close the tab.
+ window.setTimeout(() => window.close(), 300);
}
};
diff --git a/tests/unit/customizations/sso/test_utils.py b/tests/unit/customizations/sso/test_utils.py
index 647132b8e143..1f1b69872976 100644
--- a/tests/unit/customizations/sso/test_utils.py
+++ b/tests/unit/customizations/sso/test_utils.py
@@ -317,6 +317,16 @@ def test_error(self):
assert actual_code is None
assert actual_state is None
+ def test_callback_page_attempts_best_effort_close(self):
+ url = self.url + '?code=1234&state=4567'
+
+ http = urllib3.PoolManager()
+ response = http.request("GET", url)
+
+ self.fetcher.get_auth_code_and_state()
+ assert response.status == 200
+ assert b'window.close()' in response.data
+
@mock.patch(
'awscli.customizations.sso.utils.AuthCodeFetcher._REQUEST_TIMEOUT', 0.1