Skip to content

Commit 5216955

Browse files
authored
Fix regression bug in MFA device recognition. (#145)
* Fix regression bug that removed trusted device recongnition. * Fix User-Agent field to work with Okta. * Clean up cookie passing and handling, to simplify development.
1 parent eba165e commit 5216955

File tree

10 files changed

+185
-188
lines changed

10 files changed

+185
-188
lines changed

README.md

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ your AWS accounts, returning
2323
tokens into your local `~/.aws/credentials` file.
2424

2525
## What's new
26+
2627
See [Releases](https://github.com/dowjones/tokendito/releases) for a detailed Changelog.
28+
2729
### Tokendito 2.3.0
30+
2831
Version 2.3.0 of Tokendito introduces the following new features:
29-
- Basic OIE support while forcing Classic mode.
32+
33+
- Basic OIE support while forcing Classic mode.
3034
- Misc bug fixes
3135

3236
Note: This feature currently works with locally enabled OIE organizations, but it does not for Organizations with chained Authentication in mixed OIE/Classic environments.
3337

34-
3538
### Tokendito 2.2.0
3639

3740
Version 2.2.0 of Tokendito introduces the following new features:
@@ -40,7 +43,6 @@ Version 2.2.0 of Tokendito introduces the following new features:
4043
- Support for Step-Up Authorization (by @ruhulio)
4144
- Misc bug fixes
4245

43-
4446
### Tokendito 2.1.0
4547

4648
Version 2.1.0 of Tokendito introduces the following new features:
@@ -51,9 +53,9 @@ Version 2.1.0 of Tokendito introduces the following new features:
5153
- Docker container signing to ensure you are on a 'certified' Tokendito container
5254
- Misc bug fixes
5355

54-
5556
### Tokendito 2.0.0
56-
With the release of tokendito 2.0, many changes and fixes were introduced. **It is a breaking release**: your configuration needs to be updated, the command line arguments have changed, and support for Python < 3.7 has been removed.
57+
58+
With the release of tokendito 2.0, many changes and fixes were introduced. **It is a breaking release**: your configuration needs to be updated, the command line arguments have changed, and support for Python \< 3.7 has been removed.
5759
The following changes are part of this release:
5860

5961
- Set the config file to be platform dependent, and follow the XDG standard.
@@ -71,25 +73,24 @@ Consult [additional notes](https://github.com/dowjones/tokendito/blob/main/docs/
7173

7274
## Requirements
7375

74-
- Python 3.7+, or a working Docker environment
75-
- AWS account(s) federated with Okta
76+
- Python 3.7+, or a working Docker environment
77+
- AWS account(s) federated with Okta
7678

7779
Tokendito is compatible with Python 3 and can be installed with either
7880
pip or pip3.
7981

8082
## Getting started
8183

82-
1. Install (via PyPi): `pip install tokendito`
83-
2. Run `tokendito --configure`.
84-
3. Run `tokendito`.
84+
1. Install (via PyPi): `pip install tokendito`
85+
1. Run `tokendito --configure`.
86+
1. Run `tokendito`.
8587

8688
**NOTE**: Advanced users may shorten the `tokendito` interaction to a [single
8789
command](https://github.com/dowjones/tokendito/blob/main/docs/README.md#single-command-usage).
8890

8991
Have multiple Okta tiles to switch between? View our [multi-tile
9092
guide](https://github.com/dowjones/tokendito/blob/main/docs/README.md#multi-tile-guide).
9193

92-
9394
## Docker
9495

9596
Using Docker eliminates the need to install tokendito and its requirements. We are providing experimental Docker image support in [Dockerhub](https://hub.docker.com/r/tokendito/tokendito)
@@ -98,13 +99,13 @@ Using Docker eliminates the need to install tokendito and its requirements. We a
9899

99100
Run tokendito with the `docker run` command. Tokendito supports [DCT](https://docs.docker.com/engine/security/trust/), and we encourage you to enforce image signature validation before running any containers.
100101

101-
``` shell
102+
```shell
102103
export DOCKER_CONTENT_TRUST=1
103104
```
104105

105106
then
106107

107-
``` shell
108+
```shell
108109
docker run --rm -it tokendito/tokendito --version
109110
```
110111

@@ -118,27 +119,29 @@ These can be covered by mapping a single volume to both the host and container u
118119
Be sure to set the `-it` flags to enable an interactive terminal session.
119120

120121
On Windows, you can do the following:
121-
``` powershell
122+
123+
```powershell
122124
docker run --rm -it -v "%USERPROFILE%\.aws":/app/.aws -v "%USERPROFILE%\.config":/app/.config tokendito/tokendito
123125
```
124126

125127
In a Mac OS system, you can run:
126-
``` shell
128+
129+
```shell
127130
docker run --rm -it -v "$HOME/.aws":/app/.aws -v "$HOME/.config":/app/.config tokendito/tokendito
128131
```
129132

130133
On a Linux system, however, you must specify the user and group IDs for the mount mappings to work as expected.
131134
Additionally the mount points within the container move to a different location:
132135

133-
``` shell
136+
```shell
134137
docker run --user $(id -u):$(id -g) --rm -it -v "$HOME/.aws":/.aws -v "$HOME/.config":/.config tokendito/tokendito
135138
```
136139

137140
Tokendito command line arguments are supported as well.
138141

139142
**NOTE**: In the following examples the entire home directory is exported for simplicity. This is not recommended as it exposes too much data to the running container:
140143

141-
``` shell
144+
```shell
142145
docker run --rm -it -v "$HOME":/ tokendito/tokendito \
143146
--okta-tile https://acme.okta.com/home/amazon_aws/000000000000000000x0/123 \
144147
--username username@example.com \
@@ -151,7 +154,7 @@ docker run --rm -it -v "$HOME":/ tokendito/tokendito \
151154

152155
Tokendito profiles are supported while using containers provided the proper volume mapping exists.
153156

154-
``` shell
157+
```shell
155158
docker run --rm -ti -v "$HOME":/app tokendito/tokendito \
156159
--profile my-profile-name
157160
```

tests/functional/test_auth.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def test_generate_credentials(custom_args, config_file):
7373
f"{config.okta['username']}",
7474
"--password",
7575
f"{config.okta['password']}",
76+
"--config-file",
77+
f"{config.user['config_file']}",
78+
"--use-device-token",
7679
"--loglevel",
7780
"DEBUG",
7881
]
@@ -87,6 +90,16 @@ def test_generate_credentials(custom_args, config_file):
8790
assert '"sessionToken": "*****"' in proc["stderr"]
8891
assert proc["exit_status"] == 0
8992

93+
# Ensure the device token is written to the config file, and is correct.
94+
device_token = None
95+
match = re.search(r"(?<=okta_device_token': ')[^']+", proc["stderr"])
96+
if match:
97+
device_token = match.group(0)
98+
with open(config.user["config_file"]) as cfg:
99+
assert f"okta_device_token = {device_token}" in cfg.read()
100+
101+
# print(f"stderr: {proc['stderr']}")
102+
90103

91104
@pytest.mark.run("second")
92105
def test_aws_credentials(custom_args):

tests/unit/test_aws.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ def test_authenticate_to_roles(status_code, monkeypatch):
110110
"org": "https://acme.okta.org/",
111111
}
112112
)
113-
cookies = {"some_cookie": "some_value"}
114113

115114
with pytest.raises(SystemExit):
116-
authenticate_to_roles(pytest_config, [("http://test.url.com", "")], cookies)
115+
authenticate_to_roles(pytest_config, [("http://test.url.com", "")])

tests/unit/test_http_client.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,41 @@ def client():
1717
return HTTPClient()
1818

1919

20+
@pytest.mark.parametrize(
21+
"base_os, expected",
22+
[
23+
("Darwin", "Macintosh"),
24+
("Linux", "X11"),
25+
("Windows", "Windows"),
26+
("Unknown", "compatible"),
27+
],
28+
)
29+
def test_generate_user_agent(mocker, base_os, expected):
30+
"""Test the generate_user_agent function."""
31+
import platform
32+
33+
from tokendito.http_client import generate_user_agent
34+
35+
mocker.patch("platform.uname", return_value=(base_os, "", "pytest", "", "", ""))
36+
python_version = platform.python_version()
37+
38+
user_agent = generate_user_agent()
39+
assert user_agent == (
40+
f"{__title__}/{__version__} "
41+
f"({expected}; {base_os}/pytest) "
42+
f"Python/{python_version}; "
43+
f"requests/{requests.__version__})"
44+
)
45+
46+
2047
def test_init(client):
2148
"""Test initialization of HTTPClient instance."""
2249
# Check if the session property of the client is an instance of requests.Session
2350
assert isinstance(client.session, requests.Session)
2451

2552
# Check if the User-Agent header was set correctly during initialization
2653
expected_user_agent = f"{__title__}/{__version__}"
27-
assert client.session.headers["User-Agent"] == expected_user_agent
54+
assert str(expected_user_agent) in str(client.session.headers["User-Agent"])
2855

2956

3057
def test_set_cookies(client):
@@ -166,6 +193,10 @@ def test_get_device_token(client):
166193
# Check if the device token is set correctly in the session
167194
assert client.get_device_token() == device_token
168195

196+
# Check no device token when the cookie is not set
197+
client.session.cookies.clear()
198+
assert client.get_device_token() is None
199+
169200

170201
def test_set_device_token(client):
171202
"""Test setting device token in the session."""
@@ -174,3 +205,7 @@ def test_set_device_token(client):
174205

175206
# Check if the device token is set correctly in the session
176207
assert client.session.cookies.get("DT") == device_token
208+
209+
# Check no device token set when the cookie is not set
210+
client.session.cookies.clear()
211+
assert client.set_device_token("http://test.com", None) is None

tests/unit/test_okta.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest.mock import Mock
55

66
import pytest
7+
import requests.cookies
78
from tokendito.config import Config
89
from tokendito.http_client import HTTP_client
910

@@ -304,11 +305,11 @@ def test_push_approval(mocker, return_value, side_effect, expected):
304305
({"type": "SAML2"}, False),
305306
],
306307
)
307-
def test_is_local_auth(auth_properties, expected):
308+
def test_local_authentication_enabled(auth_properties, expected):
308309
"""Test local auth method."""
309310
from tokendito import okta
310311

311-
assert okta.is_local_auth(auth_properties) == expected
312+
assert okta.local_authentication_enabled(auth_properties) == expected
312313

313314

314315
@pytest.mark.parametrize(
@@ -486,8 +487,12 @@ def test_send_saml_response(mocker):
486487
from tokendito.config import Config
487488
from tokendito.http_client import HTTP_client
488489

490+
cookies = requests.cookies.RequestsCookieJar()
491+
cookies.set("sid", "pytestcookie")
489492
mock_response = Mock()
490-
mock_response.cookies = {"sid": "pytestcookie"}
493+
mock_response.status_code = 201
494+
mock_response.session = Mock()
495+
mock_response.session.cookies = cookies
491496

492497
saml_response = {
493498
"response": "pytestresponse",
@@ -501,7 +506,7 @@ def test_send_saml_response(mocker):
501506

502507
pytest_config = Config()
503508

504-
assert okta.send_saml_response(pytest_config, saml_response) == mock_response.cookies
509+
assert okta.send_saml_response(pytest_config, saml_response) is None
505510

506511

507512
def test_idp_auth(mocker):
@@ -522,10 +527,10 @@ def test_idp_auth(mocker):
522527
mocker.patch("tokendito.okta.saml2_authenticate", return_value=sid)
523528

524529
mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "OKTA"})
525-
assert okta.idp_auth(pytest_config) == sid
530+
assert okta.idp_auth(pytest_config) is None
526531

527532
mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "SAML2"})
528-
assert okta.idp_auth(pytest_config) == sid
533+
assert okta.idp_auth(pytest_config) is None
529534

530535
mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "UNKNOWN"})
531536
with pytest.raises(SystemExit) as error:
@@ -585,7 +590,7 @@ def test_step_up_authenticate(mocker):
585590
assert okta.step_up_authenticate(pytest_config, state_token) is False
586591

587592

588-
def test_local_auth(mocker):
593+
def test_local_authenticate(mocker):
589594
"""Test local auth method."""
590595
from tokendito import okta
591596
from tokendito.config import Config
@@ -606,7 +611,7 @@ def test_local_auth(mocker):
606611
}
607612
)
608613

609-
assert okta.local_auth(pytest_config) == "pytesttoken"
614+
assert okta.local_authenticate(pytest_config) == "pytesttoken"
610615

611616

612617
def test_saml2_authenticate(mocker):
@@ -634,5 +639,5 @@ def test_saml2_authenticate(mocker):
634639
}
635640

636641
mocker.patch("tokendito.okta.send_saml_request", return_value=saml_response)
637-
mocker.patch("tokendito.okta.send_saml_response", return_value="pytestsessionid")
638-
assert okta.saml2_authenticate(pytest_config, auth_properties) == "pytestsessionid"
642+
mocker.patch("tokendito.okta.send_saml_response", return_value=None)
643+
assert okta.saml2_authenticate(pytest_config, auth_properties) is None

tokendito/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# vim: set filetype=python ts=4 sw=4
22
# -*- coding: utf-8 -*-
33
"""Tokendito module initialization."""
4-
__version__ = "2.3.0"
4+
__version__ = "2.3.1"
55
__title__ = "tokendito"
66
__description__ = "Get AWS STS tokens from Okta SSO"
77
__long_description_content_type__ = "text/markdown"

tokendito/aws.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ def get_output_types():
4747
return ["json", "text", "csv", "yaml", "yaml-stream"]
4848

4949

50-
def authenticate_to_roles(config, urls, cookies):
50+
def authenticate_to_roles(config, urls):
5151
"""Authenticate AWS user with saml.
5252
53-
:param urls: list of tuples or tuple, with tiles info
54-
:param cookies: html cookies
53+
:param config: configuration object
54+
:param urls: list of tuples or tuple, with tiles information
5555
:return: response text
5656
5757
"""
@@ -63,7 +63,8 @@ def authenticate_to_roles(config, urls, cookies):
6363
logger.info(f"Discovering roles in {tile_count} tile{plural}.")
6464
for url, label in url_list:
6565
session_url = config.okta["org"] + "/login/sessionCookieRedirect"
66-
params = {"token": cookies.get("sessionToken"), "redirectUrl": url}
66+
token = HTTP_client.session.cookies.get("sessionToken", None)
67+
params = {"token": token, "redirectUrl": url}
6768
response = HTTP_client.get(session_url, params=params)
6869

6970
saml_response_string = response.text
@@ -74,7 +75,7 @@ def authenticate_to_roles(config, urls, cookies):
7475
if "Extra Verification" in saml_response_string and state_token:
7576
logger.info(f"Step-Up authentication required for {url}.")
7677
if okta.step_up_authenticate(config, state_token):
77-
return authenticate_to_roles(config, urls, cookies)
78+
return authenticate_to_roles(config, urls)
7879

7980
logger.error("Step-Up Authentication required, but not supported.")
8081
elif "App Access Locked" in saml_response_string:

0 commit comments

Comments
 (0)